November Happy Hour will be moved to Thursday December 5th.

Developing Custom Visitor Group Criteria

Product version:

EPiServer CMS 6 R2

Document last saved:

 

Personalization is the centerpiece of the R2 release and at the heart of the personalization function are the visitor group criteria. All products included in the R2 release contain criteria that are ready to use, but what if those critera do not fit your needs? This article describes the procedure for creating yout own visitor group criteria.

Table of Content

A Visitor Group Criterion

Visitor group criteria are the basic building blocks of a visitor group, each criterion defines a condition that is evaluated to determine if a visitor is part of the visitor group. If a sufficient number of criteria in a visitor group are fulfilled the visitor is considered to be a member of that group. For example, here is a Time of Day criterion that is fulfilled if the current time is between 8AM and 12PM except for on weekends: 

Example of a criterion UI

 

From a development standpoint, a visitor group criterion is a combination of (at least) two classes:

  • A model class that stores and persists user input from the UI.
  • A criterion class that evaluates the context and the data stored in the model to determine if the criteria is fulfilled or not.

Preparations

Make sure that your project references the following assemblies, they contain the classes you need to create criteria and models:

  • EPiServer.ApplicationModules.dll
  • System.ComponentModel.Composition.dll

If you want to use attribute based server side validation of model input you will also have to reference

  • System.ComponentModel.DataAnnotations.dll

Creating a Model Class

The model class stores and persists user input from the UI and provides the criterion class with easy access to the settings. The model class must implement ICriterionModel and, since instances of the model class will be persisted to the Dynamic Data Store, IDynamicData. The best way of implementing those interfaces is by inheriting from CriterionModelBase which contain standard implementations.

The only method you must override is CriterionModelBase.Copy, if you are working with simple value type properties it is sufficient to let your override call base.ShallowCopy. If your model has reference type properties and you want to make sure that each instance of the model has its own copy of the referenced objects you need to do extend the Copy override with more logic.

public class YourModelClass : CriterionModelBase
{
   public override ICriterionModel Copy() { return base.ShallowCopy(); }
}   

Now add public properties to your class that correspond to the settings you wish to be available in the UI - inputs will be created for all the public properties. Depending on the type of the property a suitable dojo widget (we use the dojo client side framework) will be used when editing the criterion.

public string MyString { get; set; }
public int MyInt { get; set; }

The DojoWidget Property Attribute

If you want to use another dojo widget than the default one you can control that by decorating the properties on your model class with the DojoWidget attribute. You can also use that attribute to set things like default value and where to find translations for the label. If you want even more control over the UI you can create your own Editor Template or write a Custom Script.

[DojoWidget(WidgetType = "dijit.form.FilteringSelect")]
public string MyString { get; set; }

If you want the input for a value to be a drop down list with predefined options you can do so be setting SelectionFactoryType on the DojoWidget attribute. The SelectionFactoryType should be a type that implements ISelectionFactory. The ISelectionFactory has a GetSelectListItems method that is responsible for supplying the options for the drop down.

There is an EnumSelectionFactory included in the CMS that can be used to present the options of an Enum in a drop down list. Note that when using EnumSelectionFactory you must provide translations of the Enum values - the translated values should be placed in your own XML file in the /lang directory, see Enumeration Localization for more information.

[DojoWidget(
   WidgetType = "dijit.form.FilteringSelect",
   SelectionFactoryType = typeof(EnumSelectionFactory))]
public SomeEnum MyEnumSelector { get; set; }

Server Side Validation

You can add input validation to your properties by using the attribute classes in System.ComponentModel.DataAnnotations. The supported validation rules are:

  • [Required]
  • [Range(double Minimum, double maximum)]
  • [StringLength(int maximumLength)]
  • [RegularExpression(string pattern)]

[Required]
[StringLength(10)]
public string MyString { get; set; }

If you want to add custom server side validation you can implement the interface IValidateCriterionModel on your model. The IValidateCriterionModel supplies a Validate method that will be called when a visitor group containing the criterion is saved.

public class YourModelClass : CriterionModelBase, IValidateCriterionModel
{
   public string MyString { get; set; }

   public CriterionValidationResult Validate(VisitorGroup currentGroup)
   {
      if (MyString.Length > 5)
      {
         return new CriterionValidationResult(false, "MyString is too long!", "MyString");
      }

      return new CriterionValidationResult(true);
   }
   ...
}

Creating a Criterion Class

Now that you have a model class it's time to create the criterion class itself. The criterion class should evaluate the context and the data stored in the model to determine if the criteria is fulfilled or not. The connection between the criterion and model classes is created via CriterionBase - the base class that must be used for the criterion class - which is a generic class that accepts ICriterionModel parameters. The only method you must override is CriterionBase.IsMatch which is the central method for a criterion, it’s the method that will be called when evaluating if a user is a member of a visitor group.

The criterion class must also be decorated with VisitorGroupCriterion attribute - this is what identifies your class as a criterion and makes it available for use. VisitorGroupCriterion has five settings:

Category
The name of group in the criteria picker UI where this criterion will be located. Criteria with the same Category value will be grouped together.

Description
A text describing how the criterion works.

DisplayName
A short name that is used to identify the criterion in menus and visitor groups.

LanguagePath
The path in the XML language files where the strings associated with this criterion is located. See VisitorGroupCriterion Settings Localization for more information.

ScriptUrl
A URL refering to a javascript file that should be loaded when this criterion is edited. See Custom Script for more information.

[VisitorGroupCriterion(
   Category = "My Criteria Category",
   Description = "How the criterion works",
   DisplayName = "Short Name",
   LanguagePath="/xml/path/to/translations/",
   ScriptUrl="javascript-that-should-be-loaded-for-the-UI.js")]
public class YourCriterionClass: CriterionBase<YourModelClass>
{
   public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext)
   {
      // Your evaluation code here.
      // The model class instance is available via the Model property.
   }
}

Subscribing to Events

You can subscribe to specific events by overriding the Subscribe method. This allows you to gather information about events that occur prior to the call to the IsMatch method. For example, the built in Visited Page criterion needs to keep track of all pages that have been visited in the current session. The events that you can subscribe to are:

  • EndRequest
  • StartRequest
  • StartSession
  • VisitedPage

If you override the Subscribe method and attach event handlers, make sure that you also override the Unsubscribe method and detach the event handlers.

public override void Subscribe(ICriterionEvents criterionEvents)
{
   criterionEvents.StartRequest += criterionEvents_StartRequest;
}

public override void Unsubscribe(ICriterionEvents criterionEvents)
{
   criterionEvents.StartRequest -= criterionEvents_StartRequest;
}

void criterionEvents_StartRequest(object sender, CriterionEventArgs e)
{
   // Handle the event
}

UI Implementation

 

Editor Templates

If you want further control over how the UI is rendered you can add an editor template for your class. To do so create a partial mvc view, the view should be named as your model, i.e. {nameOfMyModelClass}.ascx. The application may look for the view in two places:

If your criterion is part of a registered module the first search path will be

  • {yourModuleRoot}/Views/Shared/EditorTemplates/{nameOfMyModelClass}.ascx

If the view was not found in the module folder or if your criterion is not part of a module the next search path will be

  • ~/Views/Shared/EditorTemplates/{nameOfYourModelClass}.ascx.

In your editor template you can import the EPiServer.Personalization.VisitorGroups namespace to get access to the DojoEditorFor method. The DojoEditorFor method will create a widget that handles loading and saving of values from/to a specified model class property. DojoWidgetFor will discover and make use of any DojoWidget attribute settings for model class properties. If you do not use DojoWidgetFor to create edit controls for your properties you need to handle the loading and saving of values yourself.

Here's a small example of a editor template that makes use of the DojoWidgetFor method - two different overloads are used. The first call only specifies the name of the model class attribute, the second call uses the overload with the most number of parameters. A full range of overload exist between these two extremes.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<YourModelClass>" %>
<%@ Import Namespace="EPiServer.Personalization.VisitorGroups" %>
<div>
   <span>
      <%= Html.DojoEditorFor(p => p.ModelClassProperty1)%>
   </span>
   <span>
      <%= Html.DojoEditorFor(
         p => p.ModelClassProperty2, 
         new {
            @class = "WidgetCssClass", 
            @someOtherHtmlAttribute = "value" },
         "WidgetLabelText",
         "WidgetLabelCssClass",
         DojoHtmlExtensions.LabelPosition.Right)%>
</span>
</div>

Custom Script

The VisitorGroupCriterion attribute has a ScriptUrl property. There you can specify a script file that will be used when creating the settings UI. The ScriptUrl should contain a javascript object that overrides one or more of the following methods:

createUI ()
Called to create the UI for the criterion.

validate
Called when the user tries to save a visitor group.

getSettings
Called when the visitor group is saved to gather the data to save.

uiCreated
An event that is raised when the UI has been created.

Here is an example script where all the above methods are overridden.

(function() {
   return {

      // Add an extra div to the layout.
      // prototype.createUI is called to make sure
      // that the autogenerated elements are added too.

      createUI: function(namingContainerPrefix, container, settings) {
         this.prototype.createUI.apply(this, arguments);
         var element = 
            dojo.create('div', { id: namingContainer + 'MyDiv' });

         element.innerHTML = "This is MyDiv";
         dojo.place(element, container);
      },

      // Add a validation error message if the value of MyInt is > 1000.
      // prototype.validate is called so that that we don't hide errors
      // added by the default validators.
      // See also Client Side Validation to see how you can provide
      // localized strings - like error messages.

      validate: function(namingContainer) {
         var validationErrors = 
            this.prototype.validate.apply(this, arguments);
         var myIntWidget = dijit.byId(namingContainer + 'MyInt');

         if (myIntWidget.value > 1000) {
            validationErrors.Add('Error!', myIntWidget.domNode.id);
         }

         return validationErrors;
      },

      // Multiply the value of MyInt by 10 before saving.

      getSettings: function(namingContainer) {
         var myIntValue = dijit.byId(namingContainer + 'MyInt').value;
         myIntValue *= 10;

         return {
            MyInt: myIntValue
         }
      },

      // Attach eventhandler for onmouseover.
      // The settings parameter passed to the function contains
      // the model properties and values. It is not used in this
      // example but you could, for example, retrieve the current
      // value of MyInt via settings.MyInt.

      uiCreated: function(namingContainer, setting) {
         var myIntWidget = dojo.byId(namingContainer + 'MyInt');
         dojo.connect(myIntWidget, 'onmouseover', null, 
            function() { alert('You hovered over the MyInt field.'); });
      }
   }
})();

Localization

 

VisitorGroupCriterion Settings Localization

If you want to localize either DisplayName, Category or Description when adding the VisitorGroupCriterion attribute, you do so by setting the LanguagePath property. The property will indicate a location in the language files, where the CMS will look for matching keys. If either displayname, category or description keys are found, the translation will be used in the UI.

Client Side Localization

If you need translated string on the client you have to register what string you need in the createUI method. It could look something like this:

createUI: function(namingContainerPrefix, container, settings) {
   this.languageKeys = [ 
      '/xml/path/to/languageKey1',
      '/xml/path/to/languageKey2' ];
   this.prototype.createUI.apply(this, arguments);
}

After this is done you can access the translated value by using the following syntax:

validate: function(namingContainerPrefix, validationUtil) {
   …
   this.translatedText['/xml/path/to/languageKey2']
   …
}

Enumeration Localization

If you use the EnumSelectionFactory and want the names translated you can do so by adding matching keys under the enums part of the language files. For an Enum called EPiServer.Sample.Criteria.Answer the keys should look something like this:

<enums>
   <episerver>
      <sample>
         <criteria>
            <answer>
               <yes>Oh yes!</yes>
               <no>No way!</no>
            </answer>
         </criteria>
      </sample>
   </episerver>
</enums>