Views: 5677
Number of votes: 2
Average rating:

Building custom criteria for Visitor groups in CMS 6 R2

EPiServer CMS 6 R2

As you might know the next release of EPiServer CMS is around the corner. Any day now the beta release of EPiServer CMS R2 will be publically available.

Visitor Groups

One of the new features in this version of EPiServer CMS is the increased possibilities to personalize content through Visitor Groups. Simply put, Visitor Groups are a set of criteria that control what visitors see. These criteria can developed to perform any logic, for example there are built in criteria to identify if the visitor is a returning visitor and if the visitor is located in a certain geographical area. The editor combines any number of criteria and sets their parameters to define how a Visitor Group is matched, then proceeds to mark certain content as visible to that Visitor Group.

Read more about Visitor Groups and other new features of EPiServer CMS 6 R2 beta in Allan Thræn’s blog post.

Building a custom criterion: Browser

To demonstrate how you can construct your own criterion for editors to use in their Visitor Groups definitions, here’s an example which detects what browser the visitor is using.

Creating a model

For the criterion to be able to store parameters those parameters must be defined in a model. Instances of the model (corresponding to criteria in Visitor Groups) will be stored in the Dynamic Data Store (DDS). For this to work well we need to implement the EPiServer.Data.Dynamic.IDynamicData interface, which is also required by the base class that will be handling the model. It also requires us to implement the System.ICloneable interface. Both these are really simple to implement and will probably look the same for most of your criteria models:

namespace ACME.Personalization.VisitorGroups.Criteria
{
    public class BrowserModel : IDynamicData, ICloneable
    {
        public EPiServer.Data.Identity Id { get; set; }
        public object Clone()
        {
            var model = (BrowserModel)base.MemberwiseClone();
            model.Id = Identity.NewIdentity();
            return model;
        }
    }
}

Creating the criterion

The actual criterion is a class which closes the EPiServer.Personalization.VisitorGroups.CriterionBase<T> base class by supplying the model type. The base class requires us to implement one method, IsMatch, which will be called with the current IPrincipal and HttpContextBase objects as arguments. The responsibility of this method is (surprise!) to return whether the criterion is met or not when the Visitor Group containing it is evaluated in a request.

For EPiServer to recognize our new criterion we also have to decorate it with a VisitorGroupCriterion attribute (namespace EPiServer.Personalization.VisitorGroups). This attribute is based on a class in the System.ComponentModel.Composition assembly, so we must add a reference to that assembly to our project (you can find it in the bin folder of your site after installing CMS 6 R2).

Here’s the skeleton of our criterion class:

namespace ACME.Personalization.VisitorGroups.Criteria
{
    [VisitorGroupCriterion(
        Category = "User Criteria",
        DisplayName = "Browser",
        Description = "Criterion that matches type and version of the user's browser",
        LanguagePath = "/shell/cms/visitorgroups/criteria/browser"/*,
        ScriptUrl = "ClientResources/Criteria/usercriterion.js"*/)]
    public class BrowserCriterion : CriterionBase<BrowserModel>
    {
        public override bool IsMatch(System.Security.Principal.IPrincipal principal,
                                     HttpContextBase httpContext)
        {
            throw new NotImplementedException();
        }
    }
}

As you can see the VisitorGroupCriterion has a number for properties to set. The Category is used to group similar criteria in the view where the editor selects criteria to add to a Visitor Group. The other attributes are probably self-explanatory, except the ScriptUrl which you can see is commented out in my example. That property is used to specify a javascript file containing a function which is called when the criterion loads in the user interface to set up and customize the view. We will not be using such a file in this example.

Adding something useful to the model

This far, our model doesn’t contain anything which means that the editor can not specify any parameters for the criterion. So let’s add some properties which we want the editor to set. We will let the editor select a browser, a version and a compare condition for the version. We’ll represent these using an integer and two new enums:

namespace ACME.Personalization.VisitorGroups.Criteria
{
    public enum CompareCondition
    {
        Less,
        LessOrEqual,
        Equal,
        MoreOrEqual,
        More
    }
    public enum BrowserType
    {
        IE,
        FireFox,
        Other
    }
}

There are of course more browsers and other ways of representing this, but this is just for demonstration. So let’s add the properties to our model:

public class BrowserModel : IDynamicData, ICloneable
{
    /* ... Lines omitted ...*/
    [DojoWidget(
        SelectionFactoryType = typeof(EnumSelectionFactory),
        LabelTranslationKey = "/shell/cms/visitorgroups/criteria/browser/browsertype",
        AdditionalOptions = "{ selectOnClick: true }"),
        Required]
    public BrowserType Browser { get; set; }
    [DojoWidget(
        SelectionFactoryType = typeof(EnumSelectionFactory),
        LabelTranslationKey = "/shell/cms/visitorgroups/criteria/browser/comparecondition",
        AdditionalOptions = "{ selectOnClick: true }"),
        Required]
    public CompareCondition Condition { get; set; }
    [DojoWidget(
        DefaultValue = 0,
        LabelTranslationKey = "/shell/cms/visitorgroups/criteria/browser/majorversion",
        AdditionalOptions = "{ constraints: {min: 0}, selectOnClick: true }"),
        Range(0, 0xff)]
    public int MajorVersion { get; set; }
}

The DojoWidget attribute is an EPiServer class for passing values to the Dojo javascript framework used in the user interface. We are also using the EPiServer.Web.Mvc.VisitorGroups.EnumSelectionFactory to get som help representing the enums in the user interface. Also, we use the Required and Range attributes from the System.ComponentModel.DataAnnotations namespace found in the assembly with the same name, so we must add a reference to that assembly (it should be in the GAC). Most of the attribute-settings are self-explanatory but contain some Dojo magic that another article will have to dive deeper into. Also, note the language paths. These keys as well as keys for the enum values need to be added to a language file:

<languages>
  <language name="English" id="en">
    <enums><acme><personalization><visitorgroups><criteria>
              <comparecondition>
                <less>Less than</less>
                <lessorequal>Less than or equal to</lessorequal>
                <equal>Equal to</equal>
                <moreorequal>More than or equal to</moreorequal>
                <more>More than</more>
              </comparecondition>
              <browsertype>
                <ie>Internet Explorer</ie>
                <firefox>Firefox</firefox>
                <other>Other browser</other>
              </browsertype>
    </criteria></visitorgroups></personalization></acme></enums>
    <shell><cms><visitorgroups><criteria>
            <browser>
              <browsertype>Browser type</browsertype>
              <comparecondition>With version</comparecondition>
              <majorversion>Major version</majorversion>
            </browser>
    </criteria></visitorgroups></cms></shell>
  </language>
</languages>

Adding the evaluation logic

To wrap things up we must implement the IsMatch method of our criterion class to actually do something (other than throwing an exception…). This very straight-forward in this example, here’s the code:

namespace ACME.Personalization.VisitorGroups.Criteria
{
    /* ... Lines omitted ... */
    public class BrowserCriterion : CriterionBase<BrowserModel>
    {
        public override bool IsMatch(System.Security.Principal.IPrincipal principal,
                                     HttpContextBase httpContext)
        {
            return MatchBrowserType(httpContext.Request.Browser.Browser)
                   && MatchBrowserVersion(httpContext.Request.Browser.MajorVersion);
        }
        protected virtual bool MatchBrowserVersion(int majorVersion)
        {
            switch (Model.Condition)
            {
                case CompareCondition.Less:
                    return majorVersion < Model.MajorVersion;
                case CompareCondition.LessOrEqual:
                    return majorVersion <= Model.MajorVersion;
                case CompareCondition.Equal:
                    return majorVersion == Model.MajorVersion;
                case CompareCondition.MoreOrEqual:
                    return majorVersion >= Model.MajorVersion;
                case CompareCondition.More:
                    return majorVersion > Model.MajorVersion;
                default:
                    return false;
            }
        }
        protected virtual bool MatchBrowserType(string browserType)
        {
            browserType = (browserType ?? String.Empty).ToLower();
            if (browserType.Equals("ie"))
            {
                return Model.Browser == BrowserType.IE;
            }
            else if (browserType.Equals("firefox"))
            {
                return Model.Browser == BrowserType.FireFox;
            }
            else
            {
                return Model.Browser == BrowserType.Other;
            }
        }
    }
}

Fire it up!

So, we compile and go into Online Center to try out our new criterion! We find it in the “User Criteria” category as we specified, add it to a group and set the criterion properties:

edit_supportedbrowsers

I also added a “Unsupported browsers” group matching any of Firefox less than 3, IE less than 7 or Other. We then add some conditional content to an editor:

edit_mainbody

Finally, we try it out, first using Firefox 3.6:

ff

And then using IE8 in IE7 compatibility mode:

ie

Easy as pie!

Source code

The complete source code is available in the EPiServer World Code Section.

Great job, Magnus!

Nice work Magnus. I've a feeling visitor groups are going to get a lot of attention when R2 is released so this post will be a useful reference.

Nice example Magnus!

Very nice article Magnus

Please login to comment.