Hide menu Last updated: Sep 21 2015

Developing a custom visitor group criteria

Visitor group criteria are the building blocks of a visitor group. Each criterion defines a condition that is evaluated to determine if a visitor is part of a group. If a sufficient number of visitor group criteria are fulfilled, the visitor is considered a member of that group. The following example shows a Time of Day criterion that is fulfilled if the current time is between 8 AM and 12 PM except 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, which stores and persists user input from the UI.
  • A criterion class, which evaluates the context and the data stored in the model to determine if the criteria are fulfilled.

Prerequisites

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

Creating a model class

The model class stores and persists user input from the UI and provides the criterion class with access to the settings. The model class must implement ICriterionModel, since instances of the model class will be persisted to the Dynamic Data Store, IDynamicData. The best way to implement those interfaces is to inherit from CriterionModelBase, which contains 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 you override call base.ShallowCopy. If your model has reference type properties and want to make sure that each instance of the model has its own copy of the referenced objects, you need to extend the Copy override with more logic.

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

Add public properties to your class that correspond to the settings you want available in the UI, where inputs are created for the public properties. Depending on the type of the property, a suitable dojo widget (Episerver uses the Dojo client side framework) is used when editing the criterion.

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

Using the DojoWidget property attribute

If you want to use another Dojo widget than default, you can control that by decorating the properties on your model class with the DojoWidget attribute. You also can 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, create your own Editor Template or write a Custom Script as shown.

C#
[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, set SelectionFactoryType on the DojoWidget attribute. The SelectionFactoryType is a type that implements ISelectionFactory. The ISelectionFactory has a GetSelectListItems method that is responsible for supplying options for the drop-down list.

You can use an EnumSelectionFactory included in the CMS to present the options of an Enum in a drop-down list. When you use EnumSelectionFactory, provide translations of the Enum values, and place the translated values in your own XML file in the /lang directory. See Enumeration Localization.
C#
[DojoWidget(
            WidgetType = "dijit.form.FilteringSelect",
            SelectionFactoryType = typeof(EnumSelectionFactory))]
                      public SomeEnum MyEnumSelector { get; set; }

Validating the server side

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

  • [Required]
  • [Range(double Minimum, double maximum)]
  • [StringLength(int maximumLength)]
  • [RegularExpression(string pattern)]
C#
[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 is called when you save a visitor group containing the criterion.

C#
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

After you have a model class, create the criterion class to 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 you must use for the criterion class – which is a generic class that accepts ICriterionModel parameters. You must override the CriterionBase.IsMatch method, which is the central method for a criterion; it is called when evaluating if a user is a member of a visitor group.

Decorate the criterion class with VisitorGroupCriterion attribute, which identifies your class as a criterion and makes it available for use. VisitorGroupCriterion has the following settings:

  • Category. The name of group in the criteria picker UI where this criterion is located. Criteria with the same Category value are grouped together.
  • Description. A text describing how the criterion works.
  • DisplayName. A short name that identifies 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.
  • ScriptUrl. A URL referring to a javascript file that you load when this criterion is edited.
C#
[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, which gathers 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 are visited in the current session. You can subscribe to the following events:

  • 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.

C#
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
   }

Implementing the user interface

Editor templates

If you want further control over how the UI is rendered, you can add an editor template for your class by creating a partial MVC view, and name the view as your model, (such as, {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 is {yourModuleRoot}/Views/Shared/EditorTemplates/{nameOfMyModelClass}.ascx
  • If the view is not found in the module folder or if your criterion is not part of a module, the next search path is ~/Views/Shared/EditorTemplates/{nameOfYourModelClass}.ascx

In your editor template, you can import the EPiServer.Personalization.VisitorGroups to the DojoEditorFor method. The DojoEditorFor method creates a widget that handles loading and saving of values from/to a specified model class property. DojoWidgetFor discovers and makes 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.

The following editor template shows an example that makes use of the DojoWidgetFor method, where 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 exists between these two extremes.)

C#
<%@ 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, where you can specify a script file that is 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.

The following script shows an example where these methods are overridden:

C#
(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;
                          // 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.'); });
                          }
                          }
})();

Localizing the visitor group criterion

  • VisitorGroupCriterion settings localization. If you want to localize either DisplayName, Category or Description when adding the VisitorGroupCriterion attribute, set the LanguagePath property. The property indicates a location in the language files, where the CMS looks for matching keys. If either DisplayName, Category or Description keys are found, the translation is 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, which can look similar to the following code:
    C#
    createUI: function(namingContainerPrefix, container, settings) {
               this.languageKeys = [
                '/xml/path/to/languageKey1',
                '/xml/path/to/languageKey2' ];
                this.prototype.createUI.apply(this, arguments);
               }</li>

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

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

Enumeration localization

If you use the EnumSelectionFactory and want the names translated, add matching keys under the enumerators part of the language files. For an enum called EPiServer.Sample.Criteria.Answer, the keys can look similar to the following code:

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

Comments

This was probably the worst introduction to EPiServer personalization I have seen. 

Maybe you should make some very short and simple article were you explain how to implement simple personalization criterias like showing text if user is within role: Editors

The information here in the Episerver World community is mostly targeted towards developers working with the Episerver APIs, extending the Episerver functionality. For end-user documentation, please visit the Episerver user guide on webhelp.episerver.com. See for example the Personalization section, with detailed examples of how to apply personalization criteria. 

I know that it's meant for developers, however even then this article is just way to bloated, containing to much info. It should be made into several smaller article explaining one feature at a time.

Here an article that is much better at introducing personalization for episerver developers:

Visitor groups and custom criteria in EPiServer: https://tedgustaf.com/blog/2011/create-new-visitor-group-criterion-in-episerver/

Thank for posting that link, ovisariesdk. It's very helpful.

This article should start with a simple example and then perhaps be divided into different subpages for the features. 

Hi Andreas, thanks for the feedback. We have split the Personalization topic into several different pages in the latest Developer Guide: http://world.episerver.com/documentation/developer-guides/CMS/personalization/ We will also add an example on how to create your own visitor group criteria soon, it just has to be reviewed and approved by our SMEs first.