Add Contact Id in Interactions for Non-Logged in Visitors

Hi Folks,

In previous post https://sitecorerocksblog.wordpress.com/2017/10/17/sitecore-web-forms-for-marketers-creating-custom-save-action-for-adding-contact-profile-for-non-logged-in-visitors/ we have added contact for non logged-in user in experience profile. Now we had a requirement to add same contact id in Interactions table as well.

Firstly I checked Interactions table in mongo db after submitting a form. For this I used Robomongo tool and checked Interactions table. I saw last entry in the table and saw that contact id is different in this entry then our contact id.

See below image for contact id (Contacts Table)-

ContactEntry

See below image for interaction entry (Interactions Table)-

InteractionsEntry

 

In above 2 images you can clearly see contact id is different. So, for this I searched on google and found a nice article of Brian Pedersen , so I took help from this post and added code for this in CustomUpdateContactDetails class (added this class in previous post to add contact id). I have added below line –

// Identify contact for interactions
if (!Tracker.IsActive)
return;
Tracker.Current.Session.Identify(Sitecore.Context.User.Name.ToLower() == “extranet\\anonymous”
? email
: Sitecore.Context.User.Name);

 

CustomUpdateContactDetails class including above code –

using Sitecore.Data;
using Sitecore.WFFM.Abstractions.Actions;
using Sitecore.WFFM.Actions.Base;
using Sitecore.Diagnostics;
using Sitecore.WFFM.Abstractions.Analytics;
using Sitecore.WFFM.Abstractions.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Script.Serialization;
using System.Web;
using Sitecore.Analytics;
using Sitecore.Analytics.Data;
using Sitecore.Analytics.DataAccess;
using Sitecore.Analytics.Model;
using Sitecore.Analytics.Tracking;
using Sitecore.Analytics.Tracking.SharedSessionState;
using Sitecore.Configuration;

namespace xyz.WFFMSaveAction
{
public class CustomUpdateContactDetails : WffmSaveAction
{
private const int _MAX_RETRIES = 10;
private int _retries;

private readonly IAnalyticsTracker analyticsTracker;

private readonly IAuthentificationManager authentificationManager;

private readonly ILogger logger;

private readonly IFacetFactory facetFactory;

public string Mapping
{
get;
set;
}

public CustomUpdateContactDetails(IAnalyticsTracker analyticsTracker, IAuthentificationManager authentificationManager, ILogger logger, IFacetFactory facetFactory)
{
Assert.IsNotNull(analyticsTracker, “analyticsTracker”);
Assert.IsNotNull(authentificationManager, “authentificationManager”);
Assert.IsNotNull(logger, “logger”);
Assert.IsNotNull(facetFactory, “facetFactory”);
this.analyticsTracker = analyticsTracker;
this.authentificationManager = authentificationManager;
this.logger = logger;
this.facetFactory = facetFactory;
}

public override void Execute(ID formId, AdaptedResultList adaptedFields, ActionCallContext actionCallContext = null, params object[] data)
{
this.UpdateContact(adaptedFields);
}

protected virtual void UpdateContact(AdaptedResultList fields)
{
Assert.ArgumentNotNull(fields, “adaptedFields”);
Assert.IsNotNullOrEmpty(this.Mapping, “Empty mapping xml.”);
Assert.IsNotNull(this.analyticsTracker.CurrentContact, “Tracker.Current.Contact”);

var email = string.Empty;
foreach (AdaptedControlResult acr in fields)
{
if (acr.FieldName == “examplemailcom”)
{
email = acr.Value;
}
}

if (!this.authentificationManager.IsActiveUserAuthenticated)
{
var objContact = GetOrCreateContact(email);

IEnumerable<FacetNode> enumerable = this.ParseMapping(this.Mapping, fields);
IContactFacetFactory contactFacetFactory = this.facetFactory.GetContactFacetFactory();
foreach (FacetNode current in enumerable)
{
contactFacetFactory.SetFacetValue(objContact, current.Key, current.Path, current.Value, true);
}
ReleaseAndSaveContact(objContact);

// Identify contact for interactions
if (!Tracker.IsActive)
return;
Tracker.Current.Session.Identify(Sitecore.Context.User.Name.ToLower() == “extranet\\anonymous”
? email
: Sitecore.Context.User.Name);
}
}

public IEnumerable<FacetNode> ParseMapping(string mapping, AdaptedResultList adaptedFieldResultList)
{
Assert.ArgumentNotNullOrEmpty(mapping, “mapping”);
Assert.ArgumentNotNull(adaptedFieldResultList, “adaptedFieldResultList”);
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
object[] source = (object[])javaScriptSerializer.Deserialize(mapping, typeof(object));
return (from Dictionary<string, object> item in source
let itemValue = item[“value”].ToString()
let itemId = (item.ContainsKey(“id”) && item[“id”] != null) ? item[“id”].ToString() : “Preferred”
let value = adaptedFieldResultList.GetValueByFieldID(ID.Parse(item[“key”].ToString()))
where !string.IsNullOrEmpty(value)
select new FacetNode(itemId, itemValue, value)).ToList<FacetNode>();
}

public Sitecore.Analytics.Tracking.Contact GetOrCreateContact(string userName)
{
if (IsContactInSession(userName))
return Tracker.Current.Session.Contact;

ContactRepository contactRepository = Factory.CreateObject(“tracking/contactRepository”, true) as ContactRepository;
ContactManager contactManager = Factory.CreateObject(“tracking/contactManager”, true) as ContactManager;

Assert.IsNotNull(contactRepository, “contactRepository”);
Assert.IsNotNull(contactManager, “contactManager”);

try
{
Sitecore.Analytics.Tracking.Contact contact = contactRepository.LoadContactReadOnly(userName);
LockAttemptResult<Sitecore.Analytics.Tracking.Contact> lockAttempt;

lockAttempt = contact == null ? new LockAttemptResult<Sitecore.Analytics.Tracking.Contact>(LockAttemptStatus.NotFound, null, null) : contactManager.TryLoadContact(contact.ContactId);

return GetOrCreateContact(userName, lockAttempt, contactRepository, contactManager);
}
catch (Exception ex)
{
throw new Exception(this.GetType() + ” Contact could not be loaded/created – ” + userName, ex);
}
}

private bool IsContactInSession(string userName)
{
var tracker = Tracker.Current;

if (tracker != null &&
tracker.IsActive &&
tracker.Session != null &&
tracker.Session.Contact != null &&
tracker.Session.Contact.Identifiers != null &&
tracker.Session.Contact.Identifiers.Identifier != null &&
tracker.Session.Contact.Identifiers.Identifier.Equals(userName, StringComparison.InvariantCultureIgnoreCase))
return true;

return false;
}

private Sitecore.Analytics.Tracking.Contact GetOrCreateContact(string userName, LockAttemptResult<Sitecore.Analytics.Tracking.Contact> lockAttempt, ContactRepository contactRepository, ContactManager contactManager)
{
switch (lockAttempt.Status)
{
case LockAttemptStatus.Success:
Sitecore.Analytics.Tracking.Contact lockedContact = lockAttempt.Object;
lockedContact.ContactSaveMode = ContactSaveMode.AlwaysSave;
return lockedContact;

case LockAttemptStatus.NotFound:
Sitecore.Analytics.Tracking.Contact createdContact = CreateContact(userName, contactRepository);
contactManager.FlushContactToXdb(createdContact);
// Avoid going into an infinite loop.
_retries++;
if (_retries >= _MAX_RETRIES)
throw new Exception(string.Format(“ExtendedContactRepository: Contact {0} could not be created. “, userName));
return GetOrCreateContact(userName);

default:
throw new Exception(this.GetType() + ” Contact could not be locked – ” + userName);
}
}

private Sitecore.Analytics.Tracking.Contact CreateContact(string userName, ContactRepository contactRepository)
{
Sitecore.Analytics.Tracking.Contact contact = contactRepository.CreateContact(ID.NewID);
contact.Identifiers.Identifier = userName;
contact.Identifiers.IdentificationLevel = Sitecore.Analytics.Model.ContactIdentificationLevel.Known;
contact.System.Value = 0;
contact.System.VisitCount = 0;
contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
return contact;
}

public void ReleaseAndSaveContact(Sitecore.Analytics.Tracking.Contact contact)
{
ContactManager manager = Factory.CreateObject(“tracking/contactManager”, true) as ContactManager;
if (manager == null)
throw new Exception(this.GetType() + ” Could not instantiate ” + typeof(ContactManager));
manager.SaveAndReleaseContact(contact);
ClearSharedSessionLocks(manager, contact);
}

private void ClearSharedSessionLocks(ContactManager manager, Sitecore.Analytics.Tracking.Contact contact)
{
if (HttpContext.Current != null && HttpContext.Current.Session != null)
return;

var sharedSessionStateManagerField = manager.GetType().GetField(“sharedSessionStateManager”, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.IsNotNull(sharedSessionStateManagerField, “Didn’t find field ‘sharedSessionStateManager’ in type ‘{0}’.”, typeof(ContactManager));
var sssm = (SharedSessionStateManager)sharedSessionStateManagerField.GetValue(manager);
Assert.IsNotNull(sssm, “Shared session state manager field value is null.”);

var contactLockIdsProperty = sssm.GetType().GetProperty(“ContactLockIds”, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.IsNotNull(contactLockIdsProperty, “Didn’t find property ‘ContactLockIds’ in type ‘{0}’.”, sssm.GetType());
var contactLockIds = (Dictionary<Guid, object>)contactLockIdsProperty.GetValue(sssm);
Assert.IsNotNull(contactLockIds, “Contact lock IDs property value is null.”);
contactLockIds.Remove(contact.ContactId);
}
}
}

 

Now you can see in below image contact id is same.

interactionEntry2

And that’s it. So now we have added contact in experience profile as well as added same contact id in interactions table, that was our requirement.

Happy coding 🙂

 

Advertisements

Sitecore Web Forms for Marketers – Creating Custom Save Action for Adding Contact Profile for Non-Logged In Visitors

Hi Folks,

In one of our website there was a requirement that client wanted to see contact in Experience Profile even if user is not authenticated. There is already a save action Update Contact Details by default provided by web form for marketers module. This save action will allow us to update the experience database contact information of a logged in user when they fill out the form. We simply map each form field to experience database contact field facet and that’s how the web form will know which data to store.

updatecontactdetailsaction

What if we want to use this same functionality to update the contact details of an anonymous user who fills out our web form from a campaign we are running? This is currently not something that WFFM allows us to do out of the box.

So we decided to create custom contact details action that will save the information in Experience Profile of a user who is not logged into the website. Below image is for custom save action. Path is (/sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions/)

customContactDetailsAction

For this first we have created a patch config for save action as below –

<configuration xmlns:patch=”http://www.sitecore.net/xmlconfig/&#8221; xmlns:set=”http://www.sitecore.net/xmlconfig/set/”&gt;
<sitecore>
<wffm>
<!–Save actions constructor configuration–>
<actions>
<updateCustomContactDetails type=”xyz.WFFMSaveAction.CustomUpdateContactDetails, xyz”>
<param name=”analyticsTracker” ref=”/sitecore/wffm/analytics/analyticsTracker” />
<param name=”authentificationManager” ref=”/sitecore/wffm/authentificationManager” />
<param name=”logger” ref=”/sitecore/wffm/logger” />
<param name=”facetFactory” ref=”/sitecore/wffm/analytics/facetFactory” />
</updateCustomContactDetails>
</actions>
</wffm>
</sitecore>
</configuration>

 

Now we have created class CustomUpdateContactDetails. We took reference from the post Brian’s Post to create contact. Here is the code –

using Sitecore.Data;
using Sitecore.WFFM.Abstractions.Actions;
using Sitecore.WFFM.Actions.Base;
using Sitecore.Diagnostics;
using Sitecore.WFFM.Abstractions.Analytics;
using Sitecore.WFFM.Abstractions.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Script.Serialization;
using System.Web;
using Sitecore.Analytics;
using Sitecore.Analytics.Data;
using Sitecore.Analytics.DataAccess;
using Sitecore.Analytics.Model;
using Sitecore.Analytics.Tracking;
using Sitecore.Analytics.Tracking.SharedSessionState;
using Sitecore.Configuration;

namespace xyz.WFFMSaveAction
{
public class CustomUpdateContactDetails : WffmSaveAction
{
private const int _MAX_RETRIES = 10;
private int _retries;

private readonly IAnalyticsTracker analyticsTracker;

private readonly IAuthentificationManager authentificationManager;

private readonly ILogger logger;

private readonly IFacetFactory facetFactory;

public string Mapping
{
get;
set;
}

public CustomUpdateContactDetails(IAnalyticsTracker analyticsTracker, IAuthentificationManager authentificationManager, ILogger logger, IFacetFactory facetFactory)
{
Assert.IsNotNull(analyticsTracker, “analyticsTracker”);
Assert.IsNotNull(authentificationManager, “authentificationManager”);
Assert.IsNotNull(logger, “logger”);
Assert.IsNotNull(facetFactory, “facetFactory”);
this.analyticsTracker = analyticsTracker;
this.authentificationManager = authentificationManager;
this.logger = logger;
this.facetFactory = facetFactory;
}

public override void Execute(ID formId, AdaptedResultList adaptedFields, ActionCallContext actionCallContext = null, params object[] data)
{
this.UpdateContact(adaptedFields);
}

protected virtual void UpdateContact(AdaptedResultList fields)
{
Assert.ArgumentNotNull(fields, “adaptedFields”);
Assert.IsNotNullOrEmpty(this.Mapping, “Empty mapping xml.”);
Assert.IsNotNull(this.analyticsTracker.CurrentContact, “Tracker.Current.Contact”);

var email = string.Empty;
foreach (AdaptedControlResult acr in fields)
{
if (acr.FieldName == “abc”)
{
email = acr.Value;
}
}

if (!this.authentificationManager.IsActiveUserAuthenticated)
{
var objContact = GetOrCreateContact(email);

IEnumerable<FacetNode> enumerable = this.ParseMapping(this.Mapping, fields);
IContactFacetFactory contactFacetFactory = this.facetFactory.GetContactFacetFactory();
foreach (FacetNode current in enumerable)
{
contactFacetFactory.SetFacetValue(objContact, current.Key, current.Path, current.Value, true);
}
ReleaseAndSaveContact(objContact);
}
}

public IEnumerable<FacetNode> ParseMapping(string mapping, AdaptedResultList adaptedFieldResultList)
{
Assert.ArgumentNotNullOrEmpty(mapping, “mapping”);
Assert.ArgumentNotNull(adaptedFieldResultList, “adaptedFieldResultList”);
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
object[] source = (object[])javaScriptSerializer.Deserialize(mapping, typeof(object));
return (from Dictionary<string, object> item in source
let itemValue = item[“value”].ToString()
let itemId = (item.ContainsKey(“id”) && item[“id”] != null) ? item[“id”].ToString() : “Preferred”
let value = adaptedFieldResultList.GetValueByFieldID(ID.Parse(item[“key”].ToString()))
where !string.IsNullOrEmpty(value)
select new FacetNode(itemId, itemValue, value)).ToList<FacetNode>();
}

public Sitecore.Analytics.Tracking.Contact GetOrCreateContact(string userName)
{
if (IsContactInSession(userName))
return Tracker.Current.Session.Contact;

ContactRepository contactRepository = Factory.CreateObject(“tracking/contactRepository”, true) as ContactRepository;
ContactManager contactManager = Factory.CreateObject(“tracking/contactManager”, true) as ContactManager;

Assert.IsNotNull(contactRepository, “contactRepository”);
Assert.IsNotNull(contactManager, “contactManager”);

try
{
Sitecore.Analytics.Tracking.Contact contact = contactRepository.LoadContactReadOnly(userName);
LockAttemptResult<Sitecore.Analytics.Tracking.Contact> lockAttempt;

lockAttempt = contact == null ? new LockAttemptResult<Sitecore.Analytics.Tracking.Contact>(LockAttemptStatus.NotFound, null, null) : contactManager.TryLoadContact(contact.ContactId);

return GetOrCreateContact(userName, lockAttempt, contactRepository, contactManager);
}
catch (Exception ex)
{
throw new Exception(this.GetType() + ” Contact could not be loaded/created – ” + userName, ex);
}
}

private bool IsContactInSession(string userName)
{
var tracker = Tracker.Current;

if (tracker != null &&
tracker.IsActive &&
tracker.Session != null &&
tracker.Session.Contact != null &&
tracker.Session.Contact.Identifiers != null &&
tracker.Session.Contact.Identifiers.Identifier != null &&
tracker.Session.Contact.Identifiers.Identifier.Equals(userName, StringComparison.InvariantCultureIgnoreCase))
return true;

return false;
}

private Sitecore.Analytics.Tracking.Contact GetOrCreateContact(string userName, LockAttemptResult<Sitecore.Analytics.Tracking.Contact> lockAttempt, ContactRepository contactRepository, ContactManager contactManager)
{
switch (lockAttempt.Status)
{
case LockAttemptStatus.Success:
Sitecore.Analytics.Tracking.Contact lockedContact = lockAttempt.Object;
lockedContact.ContactSaveMode = ContactSaveMode.AlwaysSave;
return lockedContact;

case LockAttemptStatus.NotFound:
Sitecore.Analytics.Tracking.Contact createdContact = CreateContact(userName, contactRepository);
contactManager.FlushContactToXdb(createdContact);
// Avoid going into an infinite loop.
_retries++;
if (_retries >= _MAX_RETRIES)
throw new Exception(string.Format(“ExtendedContactRepository: Contact {0} could not be created. “, userName));
return GetOrCreateContact(userName);

default:
throw new Exception(this.GetType() + ” Contact could not be locked – ” + userName);
}
}

private Sitecore.Analytics.Tracking.Contact CreateContact(string userName, ContactRepository contactRepository)
{
Sitecore.Analytics.Tracking.Contact contact = contactRepository.CreateContact(ID.NewID);
contact.Identifiers.Identifier = userName;
contact.Identifiers.IdentificationLevel = Sitecore.Analytics.Model.ContactIdentificationLevel.Known;
contact.System.Value = 0;
contact.System.VisitCount = 0;
contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
return contact;
}

public void ReleaseAndSaveContact(Sitecore.Analytics.Tracking.Contact contact)
{
ContactManager manager = Factory.CreateObject(“tracking/contactManager”, true) as ContactManager;
if (manager == null)
throw new Exception(this.GetType() + ” Could not instantiate ” + typeof(ContactManager));
manager.SaveAndReleaseContact(contact);
ClearSharedSessionLocks(manager, contact);
}

private void ClearSharedSessionLocks(ContactManager manager, Sitecore.Analytics.Tracking.Contact contact)
{
if (HttpContext.Current != null && HttpContext.Current.Session != null)
return;

var sharedSessionStateManagerField = manager.GetType().GetField(“sharedSessionStateManager”, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.IsNotNull(sharedSessionStateManagerField, “Didn’t find field ‘sharedSessionStateManager’ in type ‘{0}’.”, typeof(ContactManager));
var sssm = (SharedSessionStateManager)sharedSessionStateManagerField.GetValue(manager);
Assert.IsNotNull(sssm, “Shared session state manager field value is null.”);

var contactLockIdsProperty = sssm.GetType().GetProperty(“ContactLockIds”, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.IsNotNull(contactLockIdsProperty, “Didn’t find property ‘ContactLockIds’ in type ‘{0}’.”, sssm.GetType());
var contactLockIds = (Dictionary<Guid, object>)contactLockIdsProperty.GetValue(sssm);
Assert.IsNotNull(contactLockIds, “Contact lock IDs property value is null.”);
contactLockIds.Remove(contact.ContactId);
}
}
}

After this code we tried to submit a form on the website and after some time searched for our contact in Experience Profile, we found our contact there, that’s it, we are able to add non-logged in user contact in Experience Profile. If you have any question then please contact me.

If you want this newly created contact id entry in Interactions table in mongodb then read my next blog.

Happy coding 🙂

Event queue stops working in CD

Hi Folks,

In one of our website we faced an issue that our indexes were not rebuilding on CD environment and event queue stopped working. Our sitecore version was 8.1 update-1 (rev. 151207) and it was a content heavy site. Then first we decided to check index strategies for web index.  For this we went to App_Config/Sitecore.ContentSearch.Lucene.Index.Web.config config file and in <strategies hint=”list:AddStrategy”> section these 2 strategies were already set –

<strategy ref=”contentSearch/indexConfigurations/indexUpdateStrategies/onPublishEndAsync” />
<strategy ref=”contentSearch/indexConfigurations/indexUpdateStrategies/remoteRebuild” />

 

After this we decided to check ScalabilitySettings.config settings. We have verified that all things were good. You can check scalability setting in below url, good discussion here on Sitcore Stackexchange – https://sitecore.stackexchange.com/questions/1052/onpublishendasync-not-triggering-on-cd-server.

 

But still issue was same for us. So we decided to dig more into it. Then we noticed an error in logs –

Hosting Environment Stop Requested

So it was clear that it was not the issue of sitecore. So we checked IIS app pool settings. For this go to IIS and click on Application Pool on the left and click on your hosted site, after this click on Advanced Settings on the right, a pop up will open and there we saw that Idle Time-out(minutes) setting was set to 20.

AppPool_

 

We changed this Idle Time-out(minutes) setting to 0. And that’s it.

Why we have changed this setting?

Since it was content heavy site and indexing was taking time more than 20 minutes. So after 20 minutes there were timeout, that causes the issue. After setting it to 0 there was no timeout, that solved our issue.

Happy coding 🙂

 

 

Newmover Module Multilingual Item Versioning Issue

Normally we use Newsmover module to manage News and Events. You can find this module at Sitecore Marketplace https://marketplace.sitecore.net/en/Modules/N/News_mover.aspx.

In one of our project we have multilingual site. We have created item in english version, that is okay..it was following proper folder structure like –

NewsMover.png

But when we are creating version of that item like this –

NewsMover4.png

There is a strange behavior. Every new version is creating item in Today’s date. Like I am creating version of above item but instead of creating item version it is creating a new item in today’s date, today date is 29/11/2016 then it is creating folder structure like this –

  • 2016
    • 11
      • 29
        • Item With New Version

 

So we decided to look into the issue also searched on google but didn’t get anything about this. Then decided to take a look at the Template Field that is associated with NewsMover event. In my case Published Date is the field. We clicked on Published Date field and in right side in Data section there was a checkbox “Shared” and we saw it was unchecked, we just checked it and decided to try creating item version now.

NewsMover3.png

And yes its working now. New item version is not creating any extra folder now. So finally that was the answer, we need to check Shared checkbox for that particular field.

 

Note – But there is problem with this trick, you can not change date for different Item’s version. Lets say if you change event date for English version then it will change for all other item’s language version as well. So please take care of this if you want different events dates for different Item’s version.

Happy Coding 🙂

First ever Sitecore Technical User Group Event in Jaipur by SUGJPR

SUGJPR happy to announce our first upcoming event on Saturday, November 19th, 2016 from 2.00 PM – 6.00 pm. Venue – Hotel Sarovar Portico Vaishali nagar, Jaipur

sugjpr

Agenda: 

  • Sitecore® Experience Platform™ – How you can own experience -Explore the power of Sitecore and learn Features, Uses of Sitecore and how it is better then other CMSs.
  • Getting started with Sitecore – Building A Very Simple Website – Basic of Sitecore installation, configuration and tuning of Sitecore in .NET environment, design and implement templates & layouts for a very simple website.
  • Sitecore Experience Accelerator (SXA) -A set of toolings and processes to allow the rapid creation of websites by increasing the amount of work that can be done in parallel.

schedule Details- 

  • 2:00PM – 2:15PM – Welcome/Registration
  • 2:15PM – 2:30PM – Introduction
  • 2.30PM – 3:15PM – Presentation – Introduction to Sitecore® Experience Platform™
  • 3.15PM – 3:30PM – Break
  • 3:30PM – 4:15PM – Presentation -Getting started with Sitecore – Building A Very Simple Website
  • 4:15PM – 4:45PM – Snacks and Tea
  • 4:45PM – 6:00PM – Presentation -Sitecore Experience Accelerator (SXA)

Please join us  – https://www.meetup.com/Sitecore-User-Group-Jaipur-Meetup/events/235463719/

If you have any questions. Please contact us –

  • email – sitecoreugjaipur@gmail.com
  • Mahendra – +91 92525 82898
  • Gaurav – +91 80581 54358
  • Office – 0141-2358028

Resolve Wildcard (*) Item

In one of our #Sitecore project we have requirement that we need to keep Events at a common place outside the Home node. So we thought to keep them in common Globals Folder. Now the biggest challenge is that how to resolve these events.

From where we got an idea of wildcard item (*). This is the Sitecore structure.

  • sitecore
    • content
      • SiteItem1
        • Home
          • Event Landing Page
            • *
        • Globals
          • Site Setting Item
          • Event Folder
            • Event Detail Page1
            • Event Detail Page2

So according to this structure we have #Wildcard Item(*) under home node. It is like event detail page that is under Event landing page.

 

WildcardItem.png

 

And in Globals folder we have all events, those are actual Event Detail Pages, we need to resolve.

 

WildcardEvents.png

Also you can see that I have created a Site Setting Item, in which I have some settings as you can see below image –

 

WildcardSiteSettingItem.png

 

On site setting item I have 2 fields –

  1. Event Calendar Root
  2. Fallback Events Detail Page

 

Okay, Its time for code now. First of all I have created a custom LinkProvider.config. That is as follows –

<?xml version=”1.0″?>
<configuration xmlns:patch=”http://www.sitecore.net/xmlconfig/”&gt;
<sitecore>
<pipelines>
<overrideItemUrl>
<processor type=”Sitecore.xyz.Pipelines.UrlOverrides.EventUrlOverride, Sitecore.EventCalendar” />
</overrideItemUrl>
</pipelines>
<linkManager>
<patch:attribute name=”defaultProvider”>fullcalendar</patch:attribute>
<providers>
<add name=”sitecore”>
<patch:attribute name=”name”>fullcalendar</patch:attribute>
<patch:attribute name=”type”>Sitecore.xyz.Links.LinkProvider, Sitecore.EventCalendar</patch:attribute>
<patch:attribute name=”lowercaseUrls”>true</patch:attribute>
<patch:attribute name=”languageEmbedding”>never</patch:attribute>
</add>
</providers>
</linkManager>
</sitecore>
</configuration>

 

As you can see in this LinkProvider.config, I have created a pipeline named overrideItemUrl and then created a defaultprovider. So first talk about defaultprovioder. I have created a provider with the name “xyz”, in which I am calling a class “LinkProvider“. The code for this LinkProvider is here –

 

public class LinkProvider : Sitecore.Links.LinkProvider
{
public override string GetItemUrl(Item item, UrlOptions options)
{
var args = new WildcardItemUrlArgs(item, options)
{
BaseAction = base.GetItemUrl
};

CorePipeline.Run(“overrideItemUrl”, args);

if (args.HasOverride && !string.IsNullOrEmpty(args.Url))
{
return args.Url;
}

return base.GetItemUrl(item, options);
}

public override UrlOptions GetDefaultUrlOptions()
{
var urlOptions = base.GetDefaultUrlOptions();
urlOptions.SiteResolving = Settings.Rendering.SiteResolving;
return urlOptions;
}
}

 

Basically Linkprovider is used to resolve links. So I am overriding its default method GetItemUrl, in which I am calling another method that is “WildcardItemUrlArgs”.

 

public class WildcardItemUrlArgs : PipelineArgs
{
public WildcardItemUrlArgs(Item item, UrlOptions urlOptions)
{
Item = item;
UrlOptions = urlOptions;
}

public Item Item { get; private set; }
public UrlOptions UrlOptions { get; private set; }
public string Url { get; set; }
public bool HasOverride { get; set; }
public Func<Item, UrlOptions, string> BaseAction { get; set; }
}

 

Then I am calling overrideItemUrl pipeline, as you can see in LinkProvider class.

 

<pipelines>
<overrideItemUrl>
<processor type=”Sitecore.xyz.Pipelines.UrlOverrides.EventUrlOverride, Sitecore.xyz” />
</overrideItemUrl>
</pipelines>

 

So now in overrideItemUrl pipeline I am calling a processor.

So now in EventUrlOverride class –

 

public class EventUrlOverride : BaseUrlOverride
{
public void Process(WildcardItemUrlArgs args)
{
if (args.HasOverride || !args.Item.InheritsTemplate(“Event Detail Page Template ID”) || Sitecore.Context.Item == null)
{
return;
}

var settingItem = Sitecore.Context.Database.GetItem(“Site Setting item id”);
if (settingItem != null)
{
if (settingItem.Fields[“Fallback Events Detail Page”] != null && !settingItem.Fields[“Fallback Events Detail Page”].Value.IsNullOrEmpty())
{
var fieldId = settingItem.Fields[“Fallback Events Detail Page”].ID;

if (!fieldId.IsNull)
{
Item detailItem = GetWildcardDetailPage(“Event landing page template id”,
fieldId);

if (detailItem != null)
{
args.Url = GetItemUrl(detailItem, args.Item, args.UrlOptions,args);
args.HasOverride = true;
}
}
}
}
}
}

 

In this class I am checking the wildcard item and trying to get it by Site setting item.

As you can see I am also inheriting a class named BaseUrlOverride in EventUrlOverride class. The code of EventUrlOverride class is here –

 

public class BaseUrlOverride
{
private string GetFriendlyUrl(Item item)
{
return item.Name.ToLower().Replace(” “, “-“);
}

protected string GetItemUrl(Item detailItem, Item targetItem, UrlOptions urlOptions, WildcardItemUrlArgs args)
{
var friendlyUrl = GetFriendlyUrl(targetItem);
return args.BaseAction(detailItem, urlOptions)
.Replace(Constants.Wildcard.UrlToken, friendlyUrl)
.Replace(Constants.Wildcard.Node, friendlyUrl);
}

protected Item GetWildcardDetailPage(string landingPageTemplateId, ID fieldId, Item currentItem = null)
{
Item detailItem = null;

var context = Context.Item;

// Handle case where context page is either the wildcard node or landing page
if (context.Name.StartsWith(Constants.Wildcard.Node) &&
context.Parent.InheritsTemplate(landingPageTemplateId))
{
detailItem = context;
}
else if (context.InheritsTemplate(landingPageTemplateId))
{
detailItem =
context.Children
.FirstOrDefault(i => i.Name.StartsWith(“*”));
}

if (detailItem == null)
{
var settingItem = Sitecore.Context.Database.GetItem(“Site setting item id”);
if (settingItem != null)
{
var siteLookupField = new LookupField(settingItem.Fields[fieldId]);
var selectedPage = siteLookupField.TargetItem;
if (selectedPage != null && selectedPage.Name.StartsWith(“*”))
{
detailItem = selectedPage;
}
}
}

return detailItem;
}
}

 

So basically in EventUrlOverride class firstly we are trying to get WildCardItem (*), then we are calling GetItemUrl method to resolve actual EventDetailPage Item.

Phewww!!! Thats it finally. Enough coding.

Happy Coding 🙂 and happy wildcard item also 🙂

Show Page Editor Message

Hi All,

In one of our project we have full page editor support. Most controls are Datasource based. So the requirement is that when there is no datasource assigned to any rendering, then we need to show a message so that anyone can identify that there is a control and they can assign datasource there. We are coding in MVC.

So for that I have created a simple Model “PageEditorModel”

namespace abc.Models
{
public class PageEditorModel
{
public string Message { get; set; }
}
}

Also created Partial View with the name “PageEditorMessage”, in which there is only message to show.

@inherits ModelView

@Model.Message

Now its time for Sitecore. I have created Dictionary for page editor message. You can see below.

pemessage

Now let’s take an example of a Rendering that is Datasource based control. In which I am checking if Datasource is null then showing PageEditor Message.

@inherits ModelView

@if (Sitecore.Mvc.Presentation.RenderingContext.Current.Rendering.DataSource.IsNullOrEmpty() && Sitecore.Context.PageMode.IsExperienceEditor)
{
@Html.Partial(“/renderingPath/PageEditorMessage.cshtml”, new PageEditorModel() { Message = RenderDictionaryValue(KSCVCPII.Domain.Common.DictionaryKeys.AssociateContentMessage).ToHtmlString() })
}
else
{
// Code here..
}

To render the dictionary value I have created a method. You can see below –

public IHtmlString RenderDictionaryValue(string value)
{
return new MvcHtmlString(HttpUtility.HtmlDecode(Sitecore.Globalization.Translate.Text(value)));
}

PEMessage2.png

So you can see the message here in the image and you can select this rendering now and can give datasource with page editor as well.

Yes that’s all.

Happy Coding 🙂

Sitecore Cache Clear Event for Multisite Solutions

Hi All,

In one of our project we have requirement to apply cache in our site. In our #Sitecore solution we have multiple Site-Nodes. Like –

Sitecore

  • -content
    • -site-node1
      • -home
    • -site-node2
      • -home

In our sitedefinition.config I have 2 sites with the name –

  • Site1
  • Site2

I have applied cache on item’s Templates presentation. Now I need to clear cache

To clear cache I have created a patch config. I used following event –

<sitecore>

<events>

<event name=”publish:end”>

<handler type=”Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel” method=”ClearCache”>

<sites hint=”list”>

<site>Site1</site>

<site>Site2</site>

</sites>

</handler>

</event>

<event name=”publish:end:remote”>

<handler type=”Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel” method=”ClearCache”>

<sites hint=”list”>

<site>Site1</site>

<site>Site2</site>

</sites>

</handler>

</event>

</events>

</sitecore>

This event clears the cache. But after some testing I found that after publishing only cache is clearing for Site2 not for Site1. To verify this, I opened the url http://hostname/sitecore/admin/showconfig.aspx and search for “publish:end” term, found the entry of Site2 only in cache clear event. Now this is the problem. Need to find a solution for this.

We need to open url http://hostname/sitecore/admin/cache.aspx every time to clear the cache, but that is not good practice. So after some digging on the google to find the solution. But finally I did it.

We need to add one more sites section in the same patch config file.

Following is the solution-

<sitecore>

<events>

<event name=”publish:end”>

<handler type=”Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel” method=”ClearCache”>

          <sites hint=”list”>

            <patch:delete />

          </sites>

<sites hint=”list”>

<site>Site1</site>

<site>Site2</site>

</sites>

 

</handler>

</event>

<event name=”publish:end:remote”>

<handler type=”Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel” method=”ClearCache”>

<sites hint=”list”>

            <patch:delete />

          </sites>

<sites hint=”list”>

<site>Site1</site>

<site>Site2</site>

</sites>

</handler>

</event>

</events>

</sitecore>

After adding this sites section, I again opened the url http://hostname/sitecore/admin/showconfig.aspx and search for publish:end term and look what I saw, I could see both sites entries (Site1 and Site2). But I needed to verify this by publishing an item as well and it worked finally for both the sites.

Happy Coding 🙂

Multiple datasource locations to Single Sublayout or Rendering in #Sitecore

Hello All,

In one of our project I faced a requirement to give multiple Datasource location to single Rendering, so that they can select whatever datasource they need. After doing some research I found that it is possible to give multiple datasource. For this first of all Login to your Sitecore.

Let’s say you have a #Sitecore structure like this –

  • Sitecore
    • -Content
      • -Site Node
        • -Globals
          • -Datasource Folder
        • -Home
          • -_Resource
            • -Datasource Folder
          • -Item 1
            • -_Resource
              • -Datasource Folder

Now we need to set 2 Datasource Locations for single Rendering. For this follow the below steps-

1)      Go to particular rendering where you want to set datasource

2)      Let’s say go to

  • -sitecore
    • -content
      • -Layout
        • -Renderings
          • -Your Rendering

3)      Click on “Your Rendering”

4)      Then in right hand side go to “Editor Options”

5)      Firstly, go to “Datasource Template” field.

Click on “Insert Link”, a pop up will open, in which you need to select “Datasource Folder Template”

datasourcelocationpe

6)      In “Editor Options” go to “Datasource Location” field

7)      Then in the “Datasource Location” write this query –

. /_Resource|query:./ancestor-or-self::*[@@templatename=Site Node Template Name’] /*[@@templatename=’Globals Folder Template Name’]/*[@@templatename=Datasource Template’]

DataourceLocationPE.png

What does this query means –

1)      This query is divided by pipe sign (|)

2)      In which “_Resource” is the first datasource location (Left part of pipe sign (|)), that is let’s say we want to give datasource to “Home” item’s rendering. So first you can see there is “_Resource” folder directly under “Home” item, so it is the path of that “_Resource” folder

3)       Now we have 2nd datasource location (Right part of pipe sign (|)), that is a query. In this query you need to set “Template Names”. Let’s say you want to set 2nd location to “Datasource Folder” that is under “Globals” Folder. So all you need to do is to set “Template Name” starting from your “Site Node” to that “Globals Folder Template”, don’t include datasource folder template in this. If your datasource is under any other node under Globals folder, then need to set template names just above your datasource folder.

For ex – Your sitecore tree is like – sitecore/content/sitenode/globals/datasource folder, so set template names starting from sitenode template to globals folder.

4)      Here it is all. Now go to “Home” item.

5)      Click on “Presentation” tab.

6)      In that click on “Details”.

7)      A pop up will open in which click on “Your Rendering”

8)      Then in the “General” tab, go to “Data Source” field and click on “Browse”, now you can see there are 2 datasource location, in which you can select datasource from either Globals Folder’s Datasource or _Resource Folder’s Datasource.

Datasource_Location.png

Happy Coding 🙂

The Add here button was not displayed in Experience Editor for a non-admin user

In one of our project I was using Sitecore 8.0 150621 revision.

And I was getting an issue on Experience Editor for a non-admin user, user unable to see Add Here button when click on component button on tab panel.

So I decided to search the cause of this.

I found below link about this –

https://dev.sitecore.net/Downloads/Sitecore%20Experience%20Platform/8%200/Sitecore%20Experience%20Platform%2080%20Update6/Release%20Notes 

The Add here button was not displayed for the MVC placeholder in Experience Editor for a non-admin user. This has been fixed. (51676, 435944)

So we decided to ask this from Sitecore Support and got the answer from there –

There is no easy way to fix it by implementing a patch, however, as a workaround you can grant your user the write-access to the /sitecore/layout/Placeholder Settings/* items: http://screencast.com/t/tHvPOC6PZTSq.

2015-04-10_1706

 

After doing this our issue was resolved.

Happy Coding 🙂