EPiServer  /  CMS December 10, 2012

Limiting content and page reference properties to values of a specific type in EPiServer CMS

By limiting available options to only valid values for PageReference and ContentReference properties in EPiServer 7 we can improve the user experience for editors. By doing so we can also protect our sites from invalid and potentially harmful editorial settings.

A fairly common scenario when building EPiServer CMS sites is that we as developers add a PageReference property to a page type whose value should be a reference to a page of a specific page type. We may for instance want editors to select a product page, a search page or a person page. Simply adding a PageReference property works but then we can’t be sure that the editor will actually set it to a page of the type we expect. Also, for editors, having to sort through the entire page tree to locate a page of a limited number of pages of that types is tedious.

A filtered page reference property

In previous versions of EPiServer it’s been tricky to achieve a solution that both validated that the selected page was of the correct type and enable editors to only have to choose amongst relevant pages. In the Alloy templates for EPiServer 7 however an elegant solution to this problem is illustrated. The ContactBlock class has a property named ContactPageLink which when rendered in edit mode looks like this:

contactpageselector

As you can see, editors are only able to select pages of a specific type and they are also able to do so by using a drop down with all those pages rather than having to sort through the entire page tree. Me like!

The code for the ContactPageLink property looks like this:

[Display(
    GroupName = SystemTabNames.Content,
    Order = 3)]
[UIHint(Global.SiteUIHints.Contact)]
public virtual PageReference ContactPageLink { get; set; }

The magic ingredient here is the UIHint. Using that the property is mapped to an “editor descriptor” named ContactPageSelector. The code for that looks like this:

[EditorDescriptorRegistration(
    TargetType = typeof(PageReference), 
    UIHint = Global.SiteUIHints.Contact)]
public class ContactPageSelector : EditorDescriptor
{
    public override void ModifyMetadata(
        ExtendedMetadata metadata, 
        IEnumerable<Attribute> attributes)
    {
        SelectionFactoryType = typeof(ContactPageSelectionFactory);
            
        ClientEditingClass = 
            "epi.cms.contentediting.editors.SelectionEditor";
  
        base.ModifyMetadata(metadata, attributes);
    }
}

The editor descriptor instructs the edit mode to render the property as a dropdown by setting the ClientEditingClass. It also hooks up the dropdown to a data source, a custom selection factory named ContactPageSelectionFactory. The code for the selection factory looks like this:

public class ContactPageSelectionFactory : ISelectionFactory
{
    public IEnumerable<ISelectItem> GetSelections(
        ExtendedMetadata metadata)
    {
        var contactPages = SiteDataFactory.Instance
            .GetContactPages();

        return new List<SelectItem>(contactPages
            .Select(c => new SelectItem
                {
                    Value = c.PageLink, 
                    Text = c.Name
                }));
    }
}

The selection factory uses more custom code in the Alloy templates to locate all pages of type ContactPage and then creates a SelectItem for each of them. These SelectItem objects are what’s used to populate the dropdown while the selected value is stored as the property’s value.

A generic approach

I really like what we’re achieving using the above code! As a developer I can ensure that editors can only insert valid values into the property. Also, given that the number of pages of the desired type is fairly small, the user experience for editors is greatly improved.

There’s just one problem. If we want to use the same approach for other properties where editors should be able to choose pages, or other types of content such as blocks, of a different type we can’t reuse any of the existing code but instead have to come up with another UI hint string, add another editor descriptor as well as another selection factory.

Instead of having to do all that tedious and error prone work I’d like a generic solution with which all we’d have to do to limit the possible selections to content of a specific type would be to add an attribute. Like this:

[ContentSelection(typeof(ProductPage))]
public virtual PageReference Product { get; set; }

[ContentSelection(typeof(ContactPage))]
public virtual PageReference ContactPage { get; set; }

[ContentSelection(typeof(TeaserBlock))]
public virtual ContentReference Teaser { get; set; }

Looks pretty nice, right? Let’s do it!

Solution

First of all we need an attribute which could only be simpler if attributes could have generic type parameters.

[AttributeUsage(
    AttributeTargets.Property, 
    AllowMultiple = false)]
public class ContentSelectionAttribute : Attribute
{
    public ContentSelectionAttribute(Type contentType)
    {
        ContentType = contentType;
    }

    public Type ContentType { get; set; }
}

The editor descriptor

With the attribute in place we can add it to properties but it won’t yet have any effect. To make the attribute matter we’ll need to create an editor descriptor.

//using System;
//using System.Collections.Generic;
//using System.Linq;
//using EPiServer.Core;
//using EPiServer.Shell.ObjectEditing;
//using EPiServer.Shell.ObjectEditing.EditorDescriptors;

[EditorDescriptorRegistration(
    TargetType = typeof(ContentReference))]
[EditorDescriptorRegistration(
    TargetType = typeof(PageReference))]
public class ContentSelector : EditorDescriptor
{
    public override void ModifyMetadata(
        ExtendedMetadata metadata, 
        IEnumerable<Attribute> attributes)
    {
        var contentSelectionAttribute = metadata.Attributes
            .OfType<ContentSelectionAttribute>()
            .SingleOrDefault();

        if(contentSelectionAttribute != null)
        {
            SelectionFactoryType =  
                typeof (ContentSelectionFactory<>)
                    .MakeGenericType(
                        contentSelectionAttribute.ContentType);

            ClientEditingClass = 
                "epi.cms.contentediting.editors.SelectionEditor";
        }

        base.ModifyMetadata(metadata, attributes);
    }
}

As opposed to the Alloy templates we register our editor descriptor for both PageReference and ContentReference and omit the UIHint. This means that it will be used for all properties of type PageReference as well as ContentReference.

In the ModifyMetadata method we check if the given property is annotated with our ContentSelection attribute. If it is we hook it up to a custom selection factory with the type specified in the attribute as type argument.

A generic selection factory

Speaking of the custom selection factory, that’s our next and final step.

//using System.Collections.Generic;
//using System.Linq;
//using EPiServer.Core;
//using EPiServer.DataAbstraction;
//using EPiServer.ServiceLocation;
//using EPiServer.Shell.ObjectEditing;

public class ContentSelectionFactory<T> : ISelectionFactory
    where T : IContentData
{
    private Injected<IContentTypeRepository> 
        ContentTypeRepository { get; set; }
    private Injected<IContentModelUsage> 
        ContentModelUsage { get; set; }
    private Injected<IContentLoader> 
        ContentLoader { get; set; }

    public IEnumerable<ISelectItem> GetSelections(
        ExtendedMetadata metadata)
    {
        var contentType = ContentTypeRepository.Service
            .Load<T>();
        if(contentType == null)
        {
            return Enumerable.Empty<SelectItem>();
        }

        var selectItems = 
            ContentModelUsage.Service
            .ListContentOfContentType(contentType)
            .Select(x => 
                x.ContentLink.CreateReferenceWithoutVersion())
            .Distinct()
            .Select(x => ContentLoader.Service.Get<T>(x))
            .OfType<IContent>()
            .Select(x => new SelectItem
                {
                    Text = x.Name,
                    Value = x.ContentLink
                })
            .OrderBy(x => x.Text)
            .ToList();
        selectItems.Insert(0, new SelectItem());
        return selectItems;
    }
}

The above code is quite the mouth full. Sorry about that. Contrary to it’s appearance however the essence of what it does is straight forward – it returns a SelectItem for each page on the site of the type specified in the type parameter. It also adds a an empty item first in the list before returning it. If it hadn’t properties with the attribute would appear prepopulated when editors create a new page and editors wouldn’t be able to set their values to null (we can always add a Required attribute if they shouldn’t be able to do that.

The desired functionality of the selection factory may vary from site to site. For instance, in a multisite scenario we’d might like to filter out content from other sites. As for the technical implementation there are several ways to achieve the same result but that’s a different blog post.

Result

With the above three classes in place we now have a generic way of creating filtered page and content reference properties whose selectable values are limited to a specific content type. As an example, the code I wanted to be able to write…

[ContentSelection(typeof(ProductPage))]
public virtual PageReference Product { get; set; }

[ContentSelection(typeof(ContactPage))]
public virtual PageReference ContactPage { get; set; }

[ContentSelection(typeof(TeaserBlock))]
public virtual ContentReference Teaser { get; set; }

… gives us this in edit mode …

multiple-content-selectors

multiple-content-selectors-expanded-1

multiple-content-selectors-expanded-2

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

My book

Want a structured way to learn EPiServer 7 development? Check out my book on Leanpub!

More about EPiServer CMS