Views: 4879
Number of votes: 1
Average rating:

Richer UI for your custom Visitor Groups criteria

In a previous blog post I described an example for a custom criterion for Visitor Groups in EPiServer CMS 6 R2. This blog post describes how you can make that example richer by using the Dojo toolkit employed by the CMS UI.

API changes

Building on the code from the previous example, I would first like to update some of the basics of it, due to API changes in the RTM of CMS 6 R2. The model can now inherit CriterionModelBase which takes care of the DDS identity and more. You only have to implement the abstract Copy method, in which you may just call the base class’ ShallowCopy() method in case you only have value members in your model or as a start of a deep copy. ShallowCopy also gives the model copy a new DDS Identity. Using these changes the RoleModel looks like this:

/// <summary>
/// Model class for use by the RoleCriterion criterion for
/// Visitor Groups. Stores a role name and a compare condition.
/// </summary>
public class RoleModel : CriterionModelBase
{
    public override ICriterionModel Copy()
    {
        return ShallowCopy();
    }
    [DojoWidget(
        SelectionFactoryType = typeof(EnumSelectionFactory),
        AdditionalOptions = "{ selectOnClick: true }",
        LabelTranslationKey = "/shell/cms/visitorgroups/criteria/role/comparecondition"
        ), Required]
    public RoleCompareCondition Condition { get; set; }
    [DojoWidget(
        WidgetType = "dijit.form.FilteringSelect",
        AdditionalOptions = "{ selectonclick: true }",
        LabelTranslationKey = "/shell/cms/visitorgroups/criteria/role/rolename"
        ), Required]
    public string RoleName { get; set; }
}
/// <summary>
/// Enum representing compare conditions for booleans
/// </summary>
public enum RoleCompareCondition
{
    Equal,
    NotEqual
}

 

Using Dojo widgets

Actually, I sneaked one other change into the RoleModel: The RoleName property now has the WidgetType property set in the DojoWidget attribute. Previously, when editing the criterion for use in a Visitor Group definition, the name of the role would have to be typed correctly into a textbox. Now I tell it to use the dijit.form.FilteringSelect control instead.

Dijit is the UI sibling of Dojo, much like jQuery UI is to jQuery. The FilteringSelect control is basically a dropdown, which can also be typed into to filter the available values in the dropdown. This is very useful in this case where there may be many roles to select from.

 

Data access

The FilteringSelect control will be reading its data (the roles to display) using an AJAX JSON call. This will be implemented using an ASP.NET MVC 2 controller. This requires a reference to System.Web.Mvc version 2 to be added to the project.

After adding that I just add a new ASP.NET MVC controller to the project. It looks like this:

[Authorize(Roles = "CmsAdmins, VisitorGroupAdmins")]
public class RoleCriterionController : Controller
{
    public JsonResult GetAllRoles()
    {
        var roles = VirtualRoles.GetAllRoles();
        return Json(new
                    {
                        identifier = "id",
                        label = "name",
                        items = roles.Select(r => new { id = r, name = r })
                    },
                    JsonRequestBehavior.AllowGet);
    }
}

First, there’s an attribute limiting the access to this controller to the necessary groups. These are the same virtual roles that have access to to the Visitor Groups editor in Online Center. Then there’s the controler with just one action/method returning a JsonResult object which will simply be seen as JSON by the caller.

The control we are going to use requires us to return a JSON object with three properties. The last one, items, is a list of the items to show in the dropwdown, each with two properties, id and name. As you can see both these are set to the same value (the name of the role). The properties outside the list, identifier and label, then define what properties inside each item in the list should be used as identifier (value in the dropdown) and label (text in the dropdown). So all this code is saying is that the dropdown should display role names and also return the role name selected.

For the EPiServer shell to register the route to this controller we have to add it to the episerver.shell section of web.config. I chose to add it in the protectedModules section with has the root path of the UI. Then add it with a unique name, which will be part of it’s path, and a reference to the assembly it resides in, for example:

<add name="ACME" resourcePath="~/">
  <assemblies>
    <add assembly="ACME.Web" />
  </assemblies>
</add>

I guess the resourcePath isn’t really needed since we use no MVC View for the controller.

Update: The resourcepath is used when resolving ScriptUrl in the VisitorGroupCriterion attribute of the criterion class. In the example below it is set to “Javascript/RoleCriterion.js” and this combined with the resourcePath gives the path where the javascript is requested: ~/JavaScript/RoleCriterion.js.

 

Wire it up

Finally, the FilteringSelect control in the criterion has to be wired up to perform the AJAX call and get the groups. This is done by referencing a javascript in the criterion class, which after that modification looks like this:

/// <summary>
/// Implementation of a EPiServer.Personalization.VisitorGroups.CriterionBase
/// which checks if a user is in a named role.
/// </summary>
[VisitorGroupCriterion(
    ScriptUrl = "JavaScript/RoleCriterion.js",
    Category = "User Criteria",
    DisplayName = "Role",
    Description = "Criterion that matches the user's roles",
    LanguagePath = "/shell/cms/visitorgroups/criteria/role")]
public class RoleCriterion : CriterionBase<RoleModel>
{
    public override bool IsMatch(System.Security.Principal.IPrincipal principal,
                                    HttpContextBase httpContext)
    {
        var isInRole = principal.IsInRole(Model.RoleName);
        var shouldBeInRole = Model.Condition == RoleCompareCondition.Equal;
        return isInRole == shouldBeInRole;
    }
}

The ~/JavaScript/RoleCriterion.js referenced by ScriptUrl looks like this:

dojo.require("epi.cms.ErrorDialog");
(function() {
    return {
        uiCreated: function(namingContainer, settings) {
            dojo.require("dojo.data.ItemFileReadStore");
            dojo.xhrGet({
                url: '../../ACME/RoleCriterion/GetAllRoles',
                handleAs: 'json',
                preventCache: true,
                error: epi.cms.ErrorDialog.showXmlHttpError,
                load: function(jsonData) {
                    var roleStore = new dojo.data.ItemFileReadStore({
                        data: jsonData
                    });
                    var roleWidget = dijit.byId(namingContainer + 'RoleName');
                    roleWidget.store = roleStore;
                    if (settings && settings.RoleName) {
                        roleWidget.set('value', settings.RoleName);
                    }
                }
            });
        }
    }
})();

Starting from the top, dojo.require is Dojo’s way of loading modules/packages, it loads EPiServer’s error dialog. Then the anonymous function returns a JSON object with one property called uiCreated which in turn is a function. This function is called (surprise!) when the controls in the UI have been created. It’s first argument, namingContainer, gives the function a seed to construct the name of controls it needs to find. The second one, settings, contains the current value stored in our model, i.e. the current value that should be set in the dropdown.

Continuning reading the code, there’s another dojo.require which loads the dojo.data.ItemFileReadStore. The ItemFileReadStore is somewhat of an analogue of an ASP.NET DataSource control. It will be given the JSON data from the GetAllRoles action and then the FilteringSelect control will connect to it to display the roles.

The dojo.xhrGet method performs the AJAX call. The path of the VisitorGroups view is [uiURL]/CMS/VisitorGroups and the web.config registration mapped the controller in [uiURL]/ACME which gives the relative url to the action method ../../ACME/RoleCriterion/GetAllRoles.

The load property in the AJAX call contains a function taking care of the result and it does the following:

  • Instantiate a new ItemFileReadStore and feed it the data returned by the AJAX call. Because the data is returned in the specific format it is understood by the ItemFileReadStore.
  • Find the FilteringSelect control created by the EPiServer shell. This is done by using the namingContainer seed and the name of the control which will be the same as the property in the model, in this case RoleName.
  • Connect the ItemFileReadStore to the FilteringSelect and use settings to set the current value, if any.

Done! When adding the criterion we will now be getting a nice AJAX-fed filtering drop down:

filteringselect1

filteringselect2

Mar 29, 2011

Magnus Rahl
( By Magnus Rahl, 3/29/2011 4:56:43 PM)

Did I get it right now, Paul? :)

smithsson68@gmail.com
( By smithsson68@gmail.com, 3/30/2011 10:34:12 AM)

Awesome Magnus!

Please login to comment.