Quantcast
Channel: Sitecore – Brian Pedersen's Sitecore and .NET Blog
Viewing all 145 articles
Browse latest View live

Sitecore contact facets – Create your own facet

$
0
0

This article describes how to create a simple Sitecore facet consisting of a DateTime and a list of strings.

A contact is made up of facets. Here are all the facets Sitecore uses (you will find the facets in \App_Config\Include\Sitecore.Analytics.Model.Config):

<facets>
  <facet name="Personal" contract="Sitecore.Analytics.Model.Entities.IContactPersonalInfo, Sitecore.Analytics.Model" />
  <facet name="Addresses" contract="Sitecore.Analytics.Model.Entities.IContactAddresses, Sitecore.Analytics.Model" />
  <facet name="Emails" contract="Sitecore.Analytics.Model.Entities.IContactEmailAddresses, Sitecore.Analytics.Model" />
  <facet name="Phone Numbers" contract="Sitecore.Analytics.Model.Entities.IContactPhoneNumbers, Sitecore.Analytics.Model" />
  <facet name="Picture" contract="Sitecore.Analytics.Model.Entities.IContactPicture, Sitecore.Analytics.Model" />
  <facet name="Communication Profile" contract="Sitecore.Analytics.Model.Entities.IContactCommunicationProfile, Sitecore.Analytics.Model" />
  <facet name="Preferences" contract="Sitecore.Analytics.Model.Entities.IContactPreferences, Sitecore.Analytics.Model" />
</facets>

In this example I will add a facet that consists of a date and a list of strings. I will call it “AvailablePublishers“.

This is a real-life example where I needed to store a list of publishers that were available the last time the user was online. Each publisher is just an ID (a string) and I store these as a list on the Contact:

Available Publishers Facet

Available Publishers Facet

It sounds simple, and it is – but there is a lot of code involved. So hang on, lets code.

STEP 1: THE BASIC INTERFACES

The “AvailablePublishers” is a Facet, the list below consists of Elements. So I need to create a IFacet interface and a IElement interface.

Here is the IFacet:

using System;
using Sitecore.Analytics.Model.Framework;

namespace PT.AvailablePublishers
{
  public interface IAvailablePublishersFacet : IFacet
  {
    IElementCollection<IAvailablePublishersElement> AvailablePublishers { get; }
    DateTime Updated { get; set; }
  }
}

The IFacet contains a list (IElementCollection) of my IElement. Here is the IElement:

using Sitecore.Analytics.Model.Framework;

namespace PT.AvailablePublishers
{
  public interface IAvailablePublishersElement : IElement, IValidatable
  {
    string PublisherID { get; set; }
  }
}

STEP 2: THE IMPLEMENTATION

Now we need concrete classes implementing IAvailablePublishersFacet and IAvailablePublishersElement:

Here is the AvailablePublishersFacet class:

using System;
using Sitecore.Analytics.Model.Framework;

namespace PT.AvailablePublishers
{
  [Serializable]
  public class AvailablePublishersFacet : Facet, IAvailablePublishersFacet
  {
    public static readonly string _FACET_NAME = "AvailablePublishers";
    private const string _UPDATED_NAME = "LastUpdated";

    public AvailablePublishersFacet()
    {
      EnsureCollection<IAvailablePublishersElement>(_FACET_NAME);
    }

    public IElementCollection<IAvailablePublishersElement> AvailablePublishers
    {
      get
      {
        return GetCollection<IAvailablePublishersElement>(_FACET_NAME);
      }
    }


    public DateTime Updated
    {
      get
      {
        return GetAttribute<DateTime>(_UPDATED_NAME);
      }
      set
      {
        SetAttribute(_UPDATED_NAME, value);
      }
    }
  }
}

and the AvailablePublishersElement class:

using System;
using Sitecore.Analytics.Model.Framework;

namespace PT.AvailablePublishers
{
  [Serializable]
  public class AvailablePublishersElement : Element, IAvailablePublishersElement
  {
    private const string _PUBLISHERID = "PublisherID";

    public AvailablePublishersElement()
    {
      EnsureAttribute<string>(_PUBLISHERID);
    }

    public string PublisherID
    {
      get
      {
        return GetAttribute<string>(_PUBLISHERID);
      }
      set
      {
        SetAttribute(_PUBLISHERID, value);
      }
    }
  }
}

Both classes are serializable.

Getting and setting properties are done using GetAttribute and SetAttribute methods retrieved from Sitecore.Analytics.Model.Framework.Element and Sitecore.Analytics.Model.Framework.Facet.

Lists are done using IElementCollection or IElementDictionary, provided by Sitecore.

STEP 3: REGISTER FACET IN SITECORE CONFIGURATION

The facets and elements are registered in Sitecore. In the configuration you also register the  IAvailablePublishersFacet as an element, even when it inherits from IFacet.

This is a simplified version of the \App_Config\Include\Sitecore.Analytics.Model.config where I have removed all other elements but my own:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <model>
      <elements>
        <element interface="PT.AvailablePublishers.IAvailablePublishersElement, MyDLL" implementation="PT.AvailablePublishers.AvailablePublishersElement, MyDLL" />
        <element interface="PT.AvailablePublishers.IAvailablePublishersFacet, MyDLL" implementation="PT.AvailablePublishers.AvailablePublishersFacet, MyDLL" />
      </elements>
      <entities>
        <contact>
          <facets>
            <facet name="AvailablePublishers" contract="PT.AvailablePublishers.IAvailablePublishersFacet, MyDLL" />
          </facets>
        </contact>
      </entities>
    </model>
  </sitecore>
</configuration>

So as you can see, both my interfaces are defined in <elements/>. The <elements/> describes the implementation of the interface, just like IOC would do it.

The facet is defined in <facets/> with a unique name. This unique name is the name you use to find the facet when you need to access it.

STEP 4: CALL THE FACET

The facet is now defined. Next step is to use the facet. To retrieve a facet you need a Contact. The contact is usually retrieved from the Sitecore Tracker:

Contact contact = Tracker.Current.Session.Contact;

This is a repository than can get the list of available publishers from a contact, and update the list of available publishers:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using PT.AvailablePublishers;
using Sitecore.Analytics.Tracking;

namespace PT.Forum.Domain.NejTakPlus.Model.Repositories
{
  public class AvailablePublishersRepository
  {
    public IEnumerable<string> Get(Contact contact)
    {
      IAvailablePublishersFacet facet = contact.GetFacet<IAvailablePublishersFacet>(AvailablePublishersFacet._FACET_NAME);
      return facet.AvailablePublishers.Select(element => element.PublisherID);
    }

    public void Set(Contact contact, IEnumerable<string> availablePublisherKeys)
    {
      IAvailablePublishersFacet facet = contact.GetFacet<IAvailablePublishersFacet>(AvailablePublishersFacet._FACET_NAME);
      facet.Updated = DateTime.Now;
      while (facet.AvailablePublishers.Count > 0)
        facet.AvailablePublishers.Remove(0);
      foreach (string availablePublisherKey in availablePublisherKeys)
      {
        facet.AvailablePublishers.Create().PublisherID = availablePublisherKey;
      }
    }
  }
}

Notice the contact.GetFacet<>() method. Here you specify the name of the facet you defined in the <facets/> section of the config file. Luckily you also define the same name in the code, so I can get the facet name from my AvailablePublishersFacet class.

Also notice the Set() method. If you wish to clear the list before inserting, you need to iterate through the list and remove them one by one. There is no Clear() method.

Remember that facets are not written to MongoDB before your session ends. So you cannot see your fact in Mongo before your session is abandoned. You can try calling:

Session.Abandon();

To force Sitecore to write the data to MongoDB.

That’s it. Happy coding.

MORE TO READ:

 



Sitecore locating sublayouts on your webpage

$
0
0

This is a followup from the post, Get Sitecore placeholders and rendering hierarchy from a Sitecore item, where I adress one of the difficulties when working with pages in Sitecore, that they tend to get very complex as one page may contain 10-30 sublayouts and renderings.
Is is very easy to loose track of which sublayout is placed where, especially if you use nested or dynamic placeholders on your pages.

My colleagues Laust Skat Nielsen and Jannik Nilsson came up with this solution to write out the name of the sublayout in the rendered HTML when in debug mode.

Their method adds a comment to the HTML when a control is rendered:

Showing the path to the control

Showing the path to the control

It is very simple to do. On PreRender, simply go through the control collection and inject the path before the usercontrol is rendered. Place the following code in the codebehind of your Layout (.aspx) file.

protected void Page_PreRender(object sender, EventArgs e)
{
  if (HttpContext.Current.IsDebuggingEnabled)
  {
    InjectControlPaths(Controls);
  }
}

protected void InjectControlPaths(ControlCollection controlCollection)
{
  foreach (Control control in controlCollection)
  {
    if (control is UserControl)
    {
      try
      {
        control.Controls.AddAt(0, new LiteralControl("<!-- Control path: " + (control as UserControl).AppRelativeVirtualPath + " -->"));
      }
      catch (Exception exception)
      {
        Sitecore.Diagnostics.Log.Debug("Failed to inject comment into " + (control as UserControl).AppRelativeVirtualPath);
      }
    }
    InjectControlPaths(control.Controls);
  }
}

Notice how the control tree is only rendered when in debugging mode. A very simple and clever solution to a common nuisance. Thanks guys.

 


Sitecore register page events

$
0
0

A Page Event in Sitecore is a way of tracking when a user have reached a goal by executing a call to action on a specific page, for example:

  • Downloaded a brochure
  • Clicked your banner
  • Done a search

A page event can be any text you like, and you can attach any data you wish.

You use the Sitecore.Analytics.Tracker to register the page event. Here is a small example on how to do it:

using System;
using Sitecore.Analytics;
using Sitecore.Analytics.Data;
using Sitecore.Analytics.Tracking;
using Sitecore.Diagnostics;

namespace MyNamespace
{
  public class RegisterPageDataService
  {
    public void RegisterPageEvent(string eventName, string text, string data, string dataKey)
    {
      if(!Tracker.Enabled)
        return;

      if (!Tracker.IsActive)
        Tracker.StartTracking();

      ICurrentPageContext currentPage = Tracker.Current.CurrentPage;
      if (currentPage == null)
        return;

      RegisterEventOnCurrentPage(eventName, text, data, dataKey, currentPage);
    }

    private static void RegisterEventOnCurrentPage(string eventName, string text, string data, string dataKey, IPageContext currentPage)
    {
      PageEventData pageEvent = new PageEventData(eventName)
        {
            Text = text,
	    Data = data ?? string.Empty,
            DataKey = string.IsNullOrEmpty(dataKey) ? string.Empty : dataKey
        };
      try
      {
        currentPage.Register(pageEvent);
      }
      catch (Exception exception)
      {
        Log.Error(string.Format("{0} pageevent not created in current Sitecore Instance: {1}", eventName, exception), typeof(RegisterPageDataService));
      }
    }
  }
}

Using the code is pretty straight forward:

// Registering a click goal
RegisterPageDataService service = new RegisterPageDataService();
service.RegisterPageEvent("Goal", "Clicked", string.Empty, string.Empty);

// Registering a search
RegisterPageDataService service = new RegisterPageDataService();
service.RegisterPageEvent("Search", "Freetext", "Pentia A/S", "Search");

The register page event ends up in the xDB (MongoDB) in the”Interactions” collection, look for “Pages” (this example is one of Sitecore’s events, not my event):

Page Event

Page Event

MORE TO READ:

 

 


Sitecore Contacts – Create and save contacts to and from xDB (MongoDB)

$
0
0

The Sitecore Contact is the cornerstone of the Sitecore Experience Platform and is the place where you store every data you know about any contact, named and anonymous.

This library was made by my colleague Peter Wind for a project we are both working on. In some cases we need to manipulate a contact that is not currently identified, for example when updating contact facets from imported data.
To do so you need to find the contact in xDB. If it does not exist, you need to create the contact. And when you are done updating the contact, you must save the data back to xDB and release the lock on the contact.

Any Contact in Sitecore is identified by a string. There is no connection between the user database (the .NET Security Provider) and a contact other than the one you make yourself. The username IS your key, and the key should be unique. You must be careful when identifying a Sitecore User, and never identify extranet\anonymous.

A WORD OF CAUTION:

The code uses some direct calls to the Sitecore Analytics and thus explores some undocumented features that was not meant to be called directly. The code is therefore a result of trial-and-error plus help from Sitecore Support. In other words: Just because it works on my 500.000+ contacts, it might fail on yours.

ENOUGH TALK LETS CODE:

using System;
using System.Collections.Generic;
using System.Text;
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;
using Sitecore.Data;
using Sitecore.Diagnostics;

namespace MyNamespace
{
  public class ExtendedContactRepository
  {
    public 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
      {
        Contact contact = contactRepository.LoadContactReadOnly(userName);
        LockAttemptResult<Contact> lockAttempt;

        if (contact == null)
          lockAttempt = new LockAttemptResult<Contact>(LockAttemptStatus.NotFound, null, null);
        else
          lockAttempt = 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);
      }
    }

    public void ReleaseAndSaveContact(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 Contact GetOrCreateContact(string userName, LockAttemptResult<Contact> lockAttempt, ContactRepository contactRepository, ContactManager contactManager)
    {
      switch (lockAttempt.Status)
      {
        case LockAttemptStatus.Success:
          Contact lockedContact = lockAttempt.Object;
          lockedContact.ContactSaveMode = ContactSaveMode.AlwaysSave;
          return lockedContact;

        case LockAttemptStatus.NotFound:
          Contact createdContact = CreateContact(userName, contactRepository);
          contactManager.FlushContactToXdb(createdContact);
          return GetOrCreateContact(userName);

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

    private Contact CreateContact(string userName, ContactRepository contactRepository)
    {
      Contact contact = contactRepository.CreateContact(ID.NewID);
      contact.Identifiers.Identifier = userName;
      contact.System.Value = 0;
      contact.System.VisitCount = 0;
      contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
      return contact;
    }

    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 void ClearSharedSessionLocks(ContactManager manager, 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);
    }
  }
}

HOW TO USE THE CODE:

// Create an instance of the repository
ExtendedContactRepository extendedContactRepository = new ExtendedContactRepository();
// Get a contact by a username
Contact contact = extendedContactRepository.GetOrCreateContact(userName);

// Do some code that updates the contact
// For example update a facet:
// https://briancaos.wordpress.com/2015/07/16/sitecore-contact-facets-create-your-own-facet/

// Save the contact
extendedContactRepository.ReleaseAndSaveContact(contact);

SOME EXPLANATION:

The 2 public methods, GetOrCreateContact() and ReleaseAndSaveContact(), are the getter and setter methods.

GetOrCreateContact() tries to get a lock on a Contact. If the lock is successful, a Contact is found and the Contact can be returned.  If not, no Contact is found and we create one.

ReleaseAndSaveContact() saves and releases the contact which means that the data is stored in the Shared Session, and the contact is released; the ClearSharedSessionLocks() attempts to release the locks from the Sitecore Shared Session  State Database. Please note that the data is still not stored directly in xDB but in the Shared Session, and data is flushed when the session expires. The trick is that we open the contact in write-mode, and release the Contact after the update, making it available immediately after by other threads.

Generally, when using the Sitecore ContactManager(), data is not manipulated directly. Only when using the Sitecore ContactRepository() you update xDB directly, but Sitecore does not recommend this, as it may have unwanted side effects.

MORE TO READ:

 


Sitecore Virtual Users – authenticate users from external systems

$
0
0

One of the very old Sitecore features popped up yesterday. My team and I are working on moving our 550.000+ users from the .NET Membership Provider to an external system, possibly .NET Identity.

.NET Identity will authorize the users, but we still need a Sitecore Membership User for authentication. To do this job, Sitecore created the Virtual User. A Virtual User is in effect a one-time, memory only Sitecore User than can be created on the fly without the use of a password.

Once the user have been authorized (username/password matches) by the external system, we can create a virtual user that Sitecore will recognize as a normal user:

// Create virtual user
var virtualUser = Sitecore.Security.Authentication.AuthenticationManager.BuildVirtualUser("extranet\\user@domain.com", true);

// You can add roles to the Virtual user
virtualUser.Roles.Add(Sitecore.Security.Accounts.Role.FromName("extranet\\MyRole"));

// You can even work with the profile if you wish
virtualUser.Profile.SetCustomProperty("CustomProperty", "12345");
virtualUser.Profile.Email = "user@domain.com";
virtualUser.Profile.Name = "My User";

// Login the virtual user
Sitecore.Security.Authentication.AuthenticationManager.LoginVirtualUser(virtualUser);

After the user have been authenticated using the LoginVirtualUser function, Sitecore will assume the identity of this user:

// This will return TRUE
Sitecore.Context.User.IsAuthenticated;

// This will return "extranet\user@domain.com"
Sitecore.Context.User.Name;

// This will return "My user"
Sitecore.Context.User.Profile.Name;

// This will return "1"
Sitecore.Context.User.Roles.Count;

// This will return "12345"
Sitecore.Context.User.Profile.GetCustomProperty("CustomProperty");

Please note that Sitecore states that if you use the User.Profile, they cannot guarantee that they will not write to the ASP.Net Membership database, although this cannot be confirmed with my current version 8.0 of Sitecore. I did not get anything written to my CORE database.

MORE TO READ:


Sitecore List Manager – Add Contacts to EXM Lists

$
0
0

This is yet another post on the focal point of Sitecore 8: Contacts. The contact repository is a multi-usage storage of user information, from visitors (named and anonymous) to imported email addresses to be used in email campaigns.

The Sitecore List Manager is a part of EXM (Email Experience Manager) and replaces .NET security roles as segmentation. In Sitecore 8, you do not need a .NET user to send a mail to a person, all you need is a Contact.

To send an email to more than one person you create a list, add Contacts, create an email, select the list and send the email.

My lists - oh what a mess. Don't worry, it's just test data.

My lists – oh what a mess. Don’t worry, it’s just test data.

A ContactList is no more than a Facet on the Contact. The facet contains the GUID of the Contact List Item and a timestamp:

ContactList Facet

ContactList Facet

So because the list is not actually a list but a set of unconnected GUID’s on unconnected Contacts, it is very easy to add and remove users from Contacts (thanks to Michael Sundstrøm for the code):

using System.Collections.Generic;
using Sitecore.Analytics.Model.Entities;
using Sitecore.Analytics.Tracking;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.SecurityModel;

namespace MyNamespace
{
  public class ContactListRepository
  {
    private const string _CONTACT_LIST_TAG_NAME = "ContactLists";

    public IEnumerable<Item> Get(Contact contact, Database database)
    {
      ITag tag = contact.Tags.Find(_CONTACT_LIST_TAG_NAME);

      if (tag == null)
        yield break;

      foreach (ITagValue tagValue in tag.Values)
      {
        yield return database.GetItem(tagValue.Value);
      }
    }

    public void Add(Contact contact, Item listItem)
    {
      using (new SecurityDisabler())
      {
        contact.Tags.Set(_CONTACT_LIST_TAG_NAME, listItem.ID.ToString());
      }
    }

    public void Remove(Contact contact, Item listItem)
    {
      using (new SecurityDisabler())
      {
        contact.Tags.Remove(_CONTACT_LIST_TAG_NAME, listItem.ID.ToString());
      }
    }

  }
}

So how to Sitecore make these unconnected GUID’s into lists? Each time you add data to the xDB (MongoDB), Sitecore updates the index, Lucene.NET or SOLR. Data is also always queried through the index which is why Sitecore does not need a separate List collection in MongoDB.

MORE TO READ:


Sitecore sublayout caching vary by cookie

$
0
0

In Sitecore you can control the caching of your sublayouts in many different ways.

Caching parameters

Caching parameters

Checking the “Cacheable” box allows you you vary the cache by data (data source), device, login (anonymous users have a different cached version from named users), param (item parameters), query string or user (each user has his own version).

Caching is an area that hasn’t changed much since Sitecore 5, which is why this code should work on any Sitecore version. However, For caching in MVC, Sitecore have implemented a different approach. See CUSTOM CACHE CRITERIA WITH MVC IN THE SITECORE ASP.NET CMS by John West for more information.

But what if you wish to vary the cache on another parameter, say, the contents of a cookie?

My colleague, Niclas Awalt, solved this for me a while ago.

STEP 1: SETTING UP SITECORE

First we need to define the name of the cookie to vary by. This is done in the “Parameters” field on the sublayout:

Parameters

The “VaryByCookie” defines my caching key, and the “Mycookie” defines the cookie to vary by.

STEP 2: CREATE A CUSTOM SUBLAYOUT

This custom sublayout contains the code that determines if the sublayout is cacheable, and if the “VaryByCookie” value is set, and if so, creates a new cache key based on the contents of the cookie:

using System.Collections.Specialized;
using System.Text;

namespace MyNamespace
{
  public class CustomSublayout : Sitecore.Web.UI.WebControls.Sublayout
  {
    private string _varyByCookies;
    private string VaryByCookies
    {
      get
      {
        return _varyByCookies ?? (_varyByCookies = GetVaryByCookiesParameter());
      }
    }

    public override string GetCacheKey()
    {
      Sitecore.Sites.SiteContext site = Sitecore.Context.Site;

      if (!Cacheable)
        return base.GetCacheKey();

      if (site != null && (!site.CacheHtml))
        return base.GetCacheKey();

      if (SkipCaching())
        return base.GetCacheKey();

      if (string.IsNullOrEmpty(VaryByCookies))
        return base.GetCacheKey();

      var cacheKey = base.GetCacheKey() + BuildCookieValueString();
      return cacheKey;
    }

    private string BuildCookieValueString()
    {
      StringBuilder stringBuilder = new StringBuilder();
      foreach (var cookieName in VaryByCookies.Split('|'))
      {
        stringBuilder.Append(GetCookieValue(cookieName, "0NoValue0"));
      }
      if (stringBuilder.Length > 0)
        stringBuilder.Insert(0, "_#cookies:");
      return stringBuilder.ToString();
    }

    private string GetCookieValue(string cookieName, string defaultValue)
    {
      var cookieValue = Context.Request.Cookies[cookieName];
      return string.Concat(cookieName, cookieValue == null ? defaultValue : cookieValue.Value);
    }

    private string GetVaryByCookiesParameter()
    {
      NameValueCollection parameters =
        Sitecore.Web.WebUtil.ParseUrlParameters(this.Parameters);

      return parameters["VaryByCookie"] ?? string.Empty;
    }
  }
}

STEP 3: HOOK UP THE NEW SUBLAYOUT:

Next step is to replace the Sitecore Sublayout rendering control with our own.

First, create a small rendering type class:

using System.Collections.Specialized;
using System.Web.UI;

namespace MyNamespace
{
  public class CustomSublayoutRenderingType : Sitecore.Web.UI.SublayoutRenderingType
  {
    public override Control GetControl(NameValueCollection parameters, bool assert)
    {
      CustomSublayout sublayout = new CustomSublayout();

      foreach (string name in parameters.Keys)
      {
        string str = parameters[name];
        Sitecore.Reflection.ReflectionUtil.SetProperty(sublayout, name, str);
      }

      return sublayout;
    }
  }
}

Then, in the web.config, in the <renderingControls> section, call our new sublayout rendering control:

<renderingControls>
  <control template="method rendering" type="Sitecore.Web.UI.WebControls.Method, Sitecore.Kernel" propertyMap="AssemblyName=assembly, ClassName=class, MethodName=method" />
  <!-- OUR NEW RENDERING -->
  <control template="sublayout" type="MyNamespace.CustomSublayoutRenderingType, MyDll" propertyMap="Path=path" />
  <!-- OUR NEW RENDERING -->
  <control template="url rendering" type="Sitecore.Web.UI.WebControls.WebPage, Sitecore.Kernel" propertyMap="Url=url" />
  <control template="xsl rendering" type="Sitecore.Web.UI.XslControlRenderingType, Sitecore.Kernel" propertyMap="Path=path" />
  <control template="webcontrol" type="Sitecore.Web.UI.WebControlRenderingType, Sitecore.Kernel" propertyMap="assembly=assembly, namespace=namespace, class=tag, properties=parameters" />
  <control template="xmlcontrol" type="Sitecore.Web.UI.XmlControlRenderingType, Sitecore.Kernel" propertyMap="controlName=control name, properties=parameters" />
</renderingControls>

Now, each time you create a sublayout, it will automatically be able to use the VaryByCookie parameter, and vary the cache based on the contents of the cookie.

MORE TO READ:


Why is session NULL in BeginRequest()? (httpRequestBegin)

$
0
0

Have you ever wondered why the session is NULL when a request begins?
The answer is simple: The Application_BeginRequest() event is fired before the Session_Start() event.
The BeginRequest() event is fired for each request as the first event. The Session_Start() event is only fired once per session, after the BeginRequest() event.

The following is specific for Sitecore:

This means that if you add a new processor in the httpRequestBegin pipeline, you will get an exception if you call HttpContext.Current.Session:

namespace MyNameSpace
{
  class MyFunction : HttpRequestProcessor
  {
    public override void Process(HttpRequestArgs args)
    {
	  // THIS WILL FAIL:
	  var somevar = HttpContext.Current.Session;
    }
  }
}

Simply because the session is not available at the time.

So what do you do then?

Sitecore have added another pipeline, httpRequestProcessed, which is fired AFTER httpRequestBegin AND Session_Start():

<httpRequestProcessed>
  <processor type="MyNamespace.MyFunction, MyDLL" />
</httpRequestProcessed>

This pipeline is usually used for replacing content or setting http status codes after the contents of a website have been processed, but you can use it for any purpose you require.

And please note that the pipeline is called for every request that have ended, so you need the same amount of carefulness as when calling httpRequestBegin. Remember to filter out doublets, Sitecore calls, check for Sitecore context etc., before calling your code.

// This is a classic (maybe a little over-the-top)
// test to see if the request is a request
// that needs processing
namespace MyNamespace
{
  public class MyClass : HttpRequestProcessor
  {
    public override void Process(HttpRequestArgs args)
    {
      Assert.ArgumentNotNull(args, "args");
      if (Context.Site == null)
        return;
      if (Context.Site.Domain != DomainManager.GetDomain("extranet"))
        return;
      if (args.Url.FilePathWithQueryString.ToUpperInvariant().Contains("redirected=true".ToUpperInvariant()))
        return; // already redirected
      if (Context.PageMode.IsPageEditor)
	    return;

      // DO CODE
    }
  }
}

MORE TO READ:

 



Sitecore xDB – flush data to MongoDB

$
0
0

When debugging Sitecore xDB issues, it is a pain that data is not written to MongoDB directly, but you have to wait for your session to end.

The legend said that you could either set the session timeout to 1 minute, or call Session.Abandon() to write to MongoDB. None of this have ever worked for me, until my colleague Lasse Pedersen found the final, bullet-proof way of having the xDB session data written to MongoDB.

Create a page with the following contents, and call it when you wish to flush data from xDB to MongoDB:

Sitecore.Analytics.Tracker.Current.EndTracking();
HttpContext.Current.Session.Abandon();

We will all pray to the debugging gods that this fix will work in every scenario.


Sitecore 8 EXM: Failed to enroll a contact in the engagement plan

$
0
0

In Sitecore 8.1 update 1, you might experience the following error when trying to send an email:

ERROR Failed to enroll a contact in the engagement plan.
Exception: System.Net.WebException
Message: The remote name could not be resolved: ‘default-cd-cluster’


Message: Recipient sc:extranet\someemail@pentia.dk skipped. Failed to enroll its corresponding contact in the engagement plan.
Source: Sitecore.EmailCampaign
at Sitecore.Modules.EmailCampaign.Core.Dispatch.DispatchTask.OnSendToNextRecipient()

The error occurs if you do not change the Analytics.ClusterName in the /App_Config/Include/Sitecore.Analytics.Tracking.config.

<setting name="Analytics.ClusterName" value="default-cd-cluster" />

The Analytics.ClusterName must be a valid, reachable URL.

<setting name="Analytics.ClusterName" value="hostname.of.my.site.com" />

This is because Sitecore calls the /sitecore/AutomationStates.ashx page using the Analytics.ClusterName as the host name.


Sitecore EXM: Send an email from code

$
0
0

The Sitecore Email Experience Manager is your way to send personalized emails to Sitecore users.   You do not need to send bulk emails, you can easily send single emails with contents like “Here is your new password” or “Your profile has been updated”.

The emails to send are “Standard Messages” so the email you create must be of the type “Triggered message”:

Triggered Message Settings

Triggered Message Settings

There is 2 ways of sending emails: To Sitecore users or Sitecore Contacts.

SEND AN EMAIL TO A SITECORE USER

Your Sitecore user must exist in the Sitecore User repository (usually as an “extranet” user).

using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Messages;
using Sitecore.Modules.EmailCampaign.Recipients;

public void Send(ID messageItemId, string userName)
{
  MessageItem message = Factory.GetMessage(messageItemId);
  Assert.IsNotNull(message, "Could not find message with ID " + messageItemId);
  RecipientId recipient = new SitecoreUserName(userName);
  Assert.IsNotNull(recipient, "Could not find recipient with username " + userName);
  new AsyncSendingManager(message).SendStandardMessage(recipient);
}

You call the function like this:

Send(new ID("{12A6D766-CA92-4303-81D2-57C66F20AB12}"), "extranet\\user@domain.com");

SEND AND EMAIL TO A CONTACT

The contact must (obviously) contain an email address. To create a contact see Sitecore Contacts – Create and save contacts to and from xDB (MongoDB). The code is near identical to the previous, but the Receipient is retrieved by resolving the contact ID:

using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Messages;
using Sitecore.Modules.EmailCampaign.Recipients;

public void Send(ID messageItemId, Guid contactID)
{
  MessageItem message = Factory.GetMessage(messageItemId);
  Assert.IsNotNull(message, "Could not find message with ID " + messageItemId);
  RecipientId recipient = RecipientRepository.GetDefaultInstance().ResolveRecipientId("xdb:" + contactID);
  Assert.IsNotNull(recipient, "Could not find recipient with ID " + contactID);
  new AsyncSendingManager(message).SendStandardMessage(recipient);
}

You call the function like this:

Send(new ID("{12A6D766-CA92-4303-81D2-57C66F20AB12}"), Guid.Parse("c3b8329b-7930-405d-8852-7a88ef4f0cb1"));

MORE TO READ:

 


Sitecore SVG files

$
0
0

In Sitecore 8.1, there is a tiny but annoying glitch in the matrix, where SVG files are not allowed in the Media Library.

But do not worry, the fix is easy. Go to your /App_Config/Sitecore.config, find the /sitecore/mediaLibrary/mediaTypes/mediaType section, and add the SVG section yourself:

<mediaType name="SVG" extensions="svg">
  <mimeType>image/svg+xml</mimeType>
  <forceDownload>false</forceDownload>
  <sharedTemplate>system/media/unversioned/image</sharedTemplate>
  <versionedTemplate>system/media/versioned/image</versionedTemplate>
  <mediaValidator type="Sitecore.Resources.Media.ImageValidator"/>
  <thumbnails>
    <generator type="Sitecore.Resources.Media.ImageThumbnailGenerator, Sitecore.Kernel">
      <extension>png</extension>
    </generator>
    <width>150</width>
    <height>150</height>
    <backgroundColor>#FFFFFF</backgroundColor>
  </thumbnails>
</mediaType>

MORE TO READ:


Sitecore EXM keeps reloading

$
0
0

Have you experienced that in Sitecore, the EXM Email Experience Manager window keeps reloading?

Email Experience Manager

Email Experience Manager

In my solution, the problem was that I have a cookie with a “:” (colon) in the cookie name. According to the old RFC 2616 specification, colons are not allowed in cookie names.

Although this is not a problem in the website or in the Sitecore Shell, it causes problems when this particular SPEAK app calls the server API.

The solution is to avoid using colons in cookie names.

 


Sitecore get host name from a different context

$
0
0

Ohno, you are running in one context, say the “shell” context, and all of your URL’s have to point to the “website” context. The hustler will obviously hard-code the domain name of the “website” into the code. But the Sitecore aficionado will check the SiteContext and retrieve the “TargetHostName” property.

This tiny class will qualify your relative URL’s to absolute URL’s, matching the domain name as specified in the <sites> section of your configuration (code simplified for readability, you should apply your own exception handling):

using System;
using System.Web;
using Sitecore.Sites;

namespace MyNameSpace
{
  internal static class FullyQualifiedUrlService
  {
    public static string Qualify(string relativeUrl, string sitename)
    {
      SiteContext sitecontext = SiteContext.GetSite(sitename);
      return Qualify(relativeUrl, sitecontext);
    }

    public static string Qualify(string relativeUrl, SiteContext sitecontext)
    {
      if (!relativeUrl.StartsWith("/"))
        relativeUrl = relativeUrl + "/";
      return string.Format("{0}://{1}{2}", sitecontext.SiteInfo.Scheme, sitecontext.TargetHostName, relativeUrl);
    }
  }
}

USAGE:

Imagine this is your sites definition for the “website” context:

<sites>
  ...
  <site name="website" targetHostName="www.mysite.com" hostName="mysite.com|www.mysite.com" scheme="http" ... .../>
  ...
</sites>

The class is called using the following code:

string relativeUrl = "/this/is/my/page";
string absoluteUrl = FullyQualifiedUrlService.Qualify(relativeUrl, "website");

And will return the following result:

http://www.mysite.com/this/is/my/page

Both the scheme and the targetHostName is resolved from the context using the Scheme and TargetHostName properties of the SiteContext.

Sitecore uses 2 properties for host resolving:

  • The “hostName” can be a pipe-separated list of domains and is used to target the number of possible URL’s that points to this context.
  • The “targetHostName” is one URL which is used internally to resolve and fully qualify your URL.

MORE TO READ:

 


Sitecore PostSessionEndPipeline failed – MongoDB.Driver.MongoQueryException

$
0
0

In my (huge) Sitecore solution we experienced the following error:

ERROR PostSessionEndPipeline failed.
Exception: MongoDB.Driver.MongoQueryException
Message: QueryFailure flag was Runner error: Overflow sort stage buffered data usage of 33570904 bytes exceeds internal limit of 33554432 bytes (response was { “$err” : “Runner error: Overflow sort stage buffered data usage of 33570904 bytes exceeds internal limit of 33554432 bytes”, “code” : 17144 }).
Source: MongoDB.Driver
at Sitecore.Analytics.Pipelines.CommitSession.SubmitSession.Process(CommitSessionPipelineArgs args)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at Sitecore.Analytics.Pipelines.PostSessionEnd.CommitSession.Process(PostSessionEndArgs args)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at Sitecore.Web.Application.RaiseSessionEndEvent(HttpApplication context)

Followed by the following message:

QueryFailure flag was Runner error: Overflow sort stage buffered data usage of xxxxxxxx bytes exceeds internal limit of 33554432 bytes (response was { “$err” : “Runner error: Overflow sort stage buffered data usage of xxxxxxxx bytes exceeds internal limit of 33554432 bytes”, “code” : 17144 }).

The error is caused by an internal 32MB memory size restriction in MongoDB.

The problem can be solved by adding an index to the Interactions collection. This will allow Sitecore to stream interactions sorted rather than attempting to load them all into memory and do the sorting in memory.

Connect to the analytics index using RoboMongo or MongoVUE and run the following query:

db.Interactions.createIndex(
{
    "StartDateTime" : 1,
    "_id" : 1
}
)

A WORD OF CAUTION:

According to MongoDB, adding indexes to a live environment does not have any side effects. But running the above query on my 7.5 million entries in the Interactions collection caused my entire Sitecore to fail (total catastrophic failure) for 5-10 minutes until the index was built.

Thank you to Sitecore Support and Joshua Wheelock for helping with the issue.

MORE TO READ:



Sitecore Advanced Configuration in .config files

$
0
0

Sitecore allows you to create configuration sections in /configuration/sitecore/ that consists of hierarchies of configurations including lists, and it will even help you serialize the configuration to object instances.

Observe the following real life example of a configuration I created to configure lists of IP adresses and IP ranges to ignore in my web application:

<pt_restrictions>
  <restriction type="Restrictions.Model.Configuration, Restrictions">
    <ipraddresses hint="raw:AddIpAddresses" >
<address ip="127.0.0.1" />
<address ip="127.0.0.2" />
    </ipraddresses>
    <ipranges hint="raw:AddIpRanges">
      <range from="192.168.0.1" to="192.168.0.255"/>
      <range from="192.169.0.1" to="192.169.0.255"/>
    </ipranges>
  </restriction>
</pt_restrictions>

The configuration section is added to a Sitecore .config file under /configuration/sitecore/.

Please notice the “type” property “Restrictions.Model.Configuration, Restrictions” and the 2 “hint” properties. These allows Sitecore to perform some reflection magic that will map the configuration section to classes.

So let’s code. First I will create an interface that outlines my configuration, a list of Ip Addresses and a list of Ip Ranges

namespace Restrictions.Model
{
  public interface IConfiguration
  {
    IEnumerable<IpAddress> IpAddresses { get; }
    IEnumerable<IpRange> IpRanges { get; }
  }
}

I need implementations of IpAddress and IpRange. The implementations contains the XML attributes as properties:

namespace Restrictions.Model
{
  public class IpAddress
  {
    public string Ip { get; set; }
  }

  public class IpRange
  {
    public string From { get; set; }
    public string To { get; set; }
  }
}

For the final touch,  I will assemble it all in a Configuration class. And here is where the magic lies. Remember the “hint=”raw:AddIpAddresses” and “hint=”raw.AddIpRanges”” attributes? These are method names in my class. Sitecore calls these for each Xml node:

using System.Collections.Generic;
using System.Xml;

namespace Restrictions.Model
{
  public class Configuration : IConfiguration
  {
    private readonly List<IpAddress> _ipAddresses = new List<IpAddress>();
    public IEnumerable<IpAddress> IpAddresses { get { return _ipAddresses; } }

    private readonly List<IpRange> _ipRanges = new List<IpRange>();
    public IEnumerable<IpRange> IpRanges  { get { return _ipRanges; } }

    protected void AddIpAddresses(XmlNode node)
    {
      if (node == null)
        return;
      if (node.Attributes == null)
        return;
      if (node.Attributes["ip"] == null)
        return;
      if (node.Attributes["ip"].Value == null)
        return;

      _ipAddresses.Add(new IpAddress() { Ip = node.Attributes["ip"].Value });
    }

    protected void AddIpRanges(XmlNode node)
    {
      if (node == null)
        return;
      if (node.Attributes == null)
        return;
      if (node.Attributes["from"] == null)
        return;
      if (node.Attributes["from"].Value == null)
        return;
      if (node.Attributes["to"] == null)
        return;
      if (node.Attributes["to"].Value == null)
        return;

      _ipRanges.Add(new IpRange() { From = node.Attributes["from"].Value, To = node.Attributes["to"].Value });
    }
  }
}

The final class is the repository that will use reflection magic to convert an XmlNode to a class implementation:

using System.Xml;

namespace Restrictions.Model.Repositories
{
  public class ConfigurationRepository
  {
    public IConfiguration GetConfiguration(string restrictionName)
    {
      XmlNode xmlNode = Sitecore.Configuration.Factory.GetConfigNode("pt_restrictions/restriction");
      return Sitecore.Configuration.Factory.CreateObject<IConfiguration>(xmlNode);
    }
  }
}

I use the class like this:

ConfigurationRepository rep = new ConfigurationRepository();
IConfiguration config = rep.GetConfiguration();

foreach (var ipaddress in config.IpAddresses)
  // do stuff;

foreach (var iprange in config.IpRanges)
  // do stuff

It looks like a big job (and it probably is) but once you get it running, it is easy to extend and use elsewhere.

MORE TO READ:

 


Sitecore Measure Pipeline performance and profile processors

$
0
0

In Sitecore, Pipelines are by far my favorite dependency injection pattern, and I have used them since Sitecore 5. One of the secret features of pipelines are the built in profiling. Oh, yes, by flipping 2 switches you can measure the performance of your pipeline processors.

STEP 1: SWITCH ON PIPELINE PROFILING

Enable the \App_Config\Include\Sitecore.PipelineProfiling.config.disabled by removing the .disabled extension.

Or add the following 2 settings to your /App_Config/sitecore.config:

<setting name="Pipelines.Profiling.Enabled" value="true" />
<setting name="Pipelines.Profiling.MeasureCpuTime" value="true" />

The Pipelines.Profiling.MeasureCpuTime is not enabled by default in the Sitecore.PipelineProfiling.config file as it introduces a performance overhead, so only use this setting on your test environment.

STEP 2: FIND THE PROFILE PAGE:

Sitecore comes with a profiling page (it has been there since Sitecore 7 i guess):

http://yourwebsite/sitecore/admin/pipelines.aspx

Pipeline Profiler

Pipeline Profiler

The page displays all pipelines executed since you pressed the refresh button, how many times it has been executed, and the average wall time (wall time = real-world time from processor started to it finished, as opposed to CPU time which is the time spend by the CPU executing the processor) per execution.

MORE TO READ:

 


Sitecore Event Queue – How to clean it – and why

$
0
0

Yesterday the dreaded Sitecore Event Queue almost killed me again – well it certainly almost killed my CM server. The server went from being busy but stable to being unresponsive. CPU and memory load skyrocketed:

A fully loaded CM server

A fully loaded CM server

Obviously it happened after a system update, so after a panic debug and rollback the system owner pointed out: “Hey, the event queue table is quite big?“.
Of course, the system updated flooded the event with 1.5 million events, and the problem did not go away because I keep 1 day of events in the queue.

SO WHAT TO DO ABOUT IT?

First we need to stabilize the system, then we need to update the configuration.

STEP 1: CLEAN OUT EVENT QUEUE, HISTORY TABLE, PUBLISH QUEUE

The following SQL statement will clean out the history table, publish queue and event queue, leaving only 12 hours of history and publish data and 4 hours of events. Replace YOURDATABASE with the name of your database:

/****** History ******/
delete FROM [YOURDATABASE_Core].[dbo].[History] where Created < DATEADD(HOUR, -12, GETDATE())
delete FROM [YOURDATABASE_Master].[dbo].[History] where Created < DATEADD(HOUR, -12, GETDATE())
delete FROM [YOURDATABASE_Web].[dbo].[History] where Created < DATEADD(HOUR, -12, GETDATE())

/****** Publishqueue ******/
delete FROM [YOURDATABASE_Core].[dbo].[PublishQueue] where Date < DATEADD(HOUR, -12, GETDATE());
delete FROM [YOURDATABASE_Master].[dbo].[PublishQueue] where Date < DATEADD(HOUR, -12, GETDATE());
delete FROM [YOURDATABASE_Web].[dbo].[PublishQueue] where Date < DATEADD(HOUR, -12, GETDATE());

/****** EventQueue ******/
delete FROM [YOURDATABASE_Master].[dbo].[EventQueue] where [Created] < DATEADD(HOUR, -4, GETDATE())
delete FROM [YOURDATABASE_Core].[dbo].[EventQueue] where [Created] < DATEADD(HOUR, -4, GETDATE())
delete FROM [YOURDATABASE_Web].[dbo].[EventQueue] where [Created] < DATEADD(HOUR, -4, GETDATE())

STEP 2: CONFIGURE THE SYSTEM TO CLEAN THE TABLES MORE OFTEN

With the system stabilized, we need to take more care of the table sizes.

HISTORY TABLE:

Sitecore is already configured to clean the tables so they only contain 12 hours of data. 12 hours of data is usually what any SQL server will handle, and you will have up to 10.000 rows in the table.

<Engines.HistoryEngine.Storage>
  <obj type="Sitecore.Data.$(database).$(database)HistoryStorage, Sitecore.Kernel">
    <param connectionStringName="$(id)" />
    <EntryLifeTime>00.12:00:00</EntryLifeTime>
  </obj>
</Engines.HistoryEngine.Storage>

PUBLISH QUEUE:

Sitecore keeps 30 days of publish queue. If you insert and update items often, you should lower this number. For each item change (including any change that the system does) is stored here.

<agent type="Sitecore.Tasks.CleanupPublishQueue, Sitecore.Kernel" method="Run" interval="04:00:00">
  <DaysToKeep>2</DaysToKeep>
</agent>

EVENT QUEUE:

The event queue is the most important table to keep small. In a distributed environment, each server will read the contents of the table every 5 seconds, using a time stamp stored in the Properties table as key. Any row before the time stamp will not be read.

You therefore need enough history to cater that a server will be offline for a while, but at the same time so little contents that any read and write will be amazingly fast.

If you can keep the number of rows below 7.000, most SQL server should be able to handle that amount of data. Even smaller numbers are preferred as well.

Before Sitecore 8.1, Sitecore would only allow you to clean events older that 1 day. This is way too much, especially if you publish often. The new IntervalToKeep will allow you to determine the hours to keep as well:

<agent type="Sitecore.Tasks.CleanupEventQueue, Sitecore.Kernel" method="Run" interval="04:00:00">
  <IntervalToKeep>04:00:00</IntervalToKeep>
  <DaysToKeep>1</DaysToKeep>
</agent>

 

THE EFFECT ON THE CLEANUP

After these changes, my server is back to normal, busy but responsive:

Normal Load

Normal Load

MORE TO READ:

 


Sitecore Publishing – Programmatically determine if item should be published

$
0
0

Sitecore uses it’s publish settings to determine if an item should be published. But you can only control a publish from versions and date times.

Sitecore Publishing Settings

Sitecore Publishing Settings

So what if you have other values that determine if an item must be published or not? Say, a status field or a checkbox, or a combination of both? And what do you do if these fields are updated from external systems?

One way is to extend the publishItem pipeline. Every item that must be published goes through this pipeline one by one and you can therefore add a new processor that determines if an item is published or not.

AN EXAMPLE:

These are our publish options: A status field and a checkbox is updated by users and external systems to determine if this item is eligible for publishing.

Publish Options

Publish Options

The publishItem pipeline works “the other way round”, meaning that if the item can be published we leave it alone, and if not we change the publish status to “DeleteTargetItem“.

Here is my new processor (some pseudocode, apply your own checks):

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Publishing;
using Sitecore.Publishing.Pipelines.PublishItem;

namespace MyCode.Infrastructure.Pipelines.PublishItem
{
  public class DetermineActionForItem : PublishItemProcessor
  {
    public override void Process(PublishItemContext context)
    {
      // First a list of checks to ensure that
      // it is our item and we can make the decision
      // about it's publish status
      if (context == null)
        return;
      if (context.Aborted)
        return;
      Item sourceItem = context.PublishHelper.GetSourceItem(context.ItemId);
      if (sourceItem == null)
        return;
      if (!sourceItem.Paths.IsContentItem)
        return;
      // I will only check our specific item
      if (!sourceItem.TemplateName == "mytemplate")
        return;

      // OK, now we know that this is our item and
      // we can determine it's faith
      // Check every language to see if it is eligible for publishing
      foreach (Language language in sourceItem.Languages)
      {
        Item languageVersion = sourceItem.Versions.GetLatestVersion(language);
        // A little pseudocode, here is the check to see if the item can be published
        if (StatusIsOK(languageVersion["StatusField"]) && CheckBoxIsChecked(languageVersion["CheckBoxField"])
        {
          // Yes, the item can be published
          return;
        }
      }

      // No, the item cannot be published, set the publishaction == DeleteTargetItem
      Log.Info(string.Format("{0}: Unpublishing Item '{1}' from database '{2}' because it is not in the correct state.",
               GetType().Name,
               AuditFormatter.FormatItem(sourceItem),
               context.PublishContext.PublishOptions.TargetDatabase),
               this);
      context.Action = PublishAction.DeleteTargetItem;
    }
  }
}

As you can see, the function returns if the item being processed is not our item, or if the item can be published. If the item is not eligible for publishing, we change the Action to DeleteTargetItem.

The processor is added to the publish pipeline just before the Sitecore “DetermineAction” processor:

<publishItem help="Processors should derive from Sitecore.Publishing.Pipelines.PublishItem.PublishItemProcessor">
  <processor type="Sitecore.Publishing.Pipelines.PublishItem.RaiseProcessingEvent, Sitecore.Kernel"/>
  <processor type="Sitecore.Publishing.Pipelines.PublishItem.CheckVirtualItem, Sitecore.Kernel"/>
  <processor type="Sitecore.Publishing.Pipelines.PublishItem.CheckSecurity, Sitecore.Kernel"/>
  <!-- Our Processor inserted before Sitecore's "DetermineAction" proecssor -->
  <processor type="MyCode.Infrastructure.Pipelines.PublishItem.DetermineActionForItem, MyDll" />
  <!-- ... -->
  <processor type="Sitecore.Publishing.Pipelines.PublishItem.DetermineAction, Sitecore.Kernel"/>
  ...
  ...
  ...
</publishItem>

MORE TO READ:

 


Sitecore Scheduled Tasks – Run on certain server instance

$
0
0

If you would like certain Sitecore Scheduled Tasks run on a certain server instance (your CM server for example) the approach is pretty simple.

First, create a new folder under /sitecore/system/Tasks. I have a new folder called “CMSchedules“. Place your Scheduled Tasks in this folder.

Scheduled Tasks in separate folder

Scheduled Tasks in separate folder

Then tell Sitecore to execute these tasks. Place the following configuration on the instance you wish should run these tasks. Do not place the configuration on servers that should not run the tasks.

You need to patch the Sitecore.config (for Sitecore 8.1 and up) or web.config (for versions prior to Sitecore 8.1). Add the following to the /sitecore/scheduling section:

<agent type="Sitecore.Tasks.DatabaseAgent" method="Run" interval="00:05:00">
  <param desc="database">master</param>
  <param desc="schedule root">/sitecore/system/Tasks/CMSchedules</param>
  <LogActivity>true</LogActivity>
</agent>

The configuration tells Sitecore to execute all tasks in the /sitecore/system/Tasks/CMSchedules every 5 minutes.

MORE TO READ:

 


Viewing all 145 articles
Browse latest View live