Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

Per Magne Skuseth
Nov 14, 2014
  8527
(0 votes)

Content Providers 101 – Part I: Introduction, Initialization, UI & Identity Mapping

A couple of weeks ago at an EPiServer Techforum in Norway, I did a demo on Content Providers. A few people have been asking about the code I wrote, so I decided to write this blog post series. While content providers is not a new feature, it has become a lot more manageable in newer versions of EPiServer. Especially when being able to create custom content types and having tools such as the identity mapping service, which will be used in this example.

 

External content

In order to create a content provider you’ll need some actual content to provide (duh!). In the following example, I'll create a content provider that will import objects from a PersonService. The PersonService really just reads and writes information to a tab delimited file, with entries that contains basic information about a person. Keep in mind that the service could have retrieved the content from anywhere, and not just from a text file.

An entry looks like this:

entry

The service converts this information into Person objects that will later be converted to Attendee objects, and used as content in EPiServer. The content will be displayed as a flat structure in a new tab in the assets pane.

Here’s how the Person class is defined:

public class Person
{
    public string Email { get; set; }
    public string Title { get; set; }
    public string Company { get; set; }
    public string Name { get; set; }
}

 

The PersonService contains methods for retrieving, updating, adding, deleting and searching for objects.

public interface IPersonService
{
    Person GetPersonByEmail(string email);
    IEnumerable<Person> GetAll();
    void UpdatePerson(string originalEmail, string newEmail, string title, string name, string company);
    void CreatePerson(Person person);
    IEnumerable<Person> Search(string searchQuery);
    void Delete(string email);
}

 

Custom IContent

As mentioned, I’ll convert the person objects into an Attendee object. The reason I’ve named it Attendees is because the list I used during the demo was based on the attendees at the EPiServer Techforum.  I’ve changed the attendee names for this example though.

[ContentType(GUID = "0D4A8F04-8337-4A59-882E-F39617E5D434")]
public class Attendee : ContentBase
{
    [EmailAddress]
    [Required]
    public virtual string Email { get; set; }
    public virtual string Title { get; set; }
    public virtual string Company { get; set; }
}

This is a custom IContent type. By inheriting ContentBase, all the necessary properties to create IContent, like Name, StartPublish, StopPublish, ContentLink  and so on, are implemented.
Instead of creating a new content type, I could have converted the person objects into standard content types as well, like a page type or a block type.

In order to convert a person to an attendee, we’ll need to populate quite a few properties, including properties found in ContentBase, like the ContentLink. When populating the ContentLink, which is a ContentReference, you need an int property. However, the person objects does not contain any suitable int properties. This is where the IdentityMappingService becomes very useful. It can create one for us!  In this case, it’s being mapped to the person’s email, and will also contain a mapped GUID. Below is an example on how to do this. I’ve added plenty of inline comments, so hopefully it will make sense.

public Attendee ConvertToAttendee(Person person)
{
    ContentType type = ContentTypeRepository.Load(typeof(Attendee));
    Attendee attendee =
        ContentFactory.CreateContent(type, new BuildingContext(type)
        {
            // as this is a flat structure, we set the parent to the provider's EntryPoint
            // by setting this in the Buildingcontext, access rights will also be inherited
            Parent = DataFactory.Instance.Get<ContentFolder>(EntryPoint),
        }) as Attendee;
 
    // make sure the content will be visible for all users
    attendee.Status = VersionStatus.Published;
    attendee.IsPendingPublish = false;
    attendee.StartPublish = DateTime.Now.Subtract(TimeSpan.FromDays(14));
 
    // This part is a bit tricky. IdentityMappingService is used in order to create the ContentReference and content GUID. 
    // The only unique property on the person object is the e-mail, so that will be used as the identifier.
    // First, create an external identifier based on the person's e-mail
    Uri externalId = MappedIdentity.ConstructExternalIdentifier(ProviderKey, person.Email);
    // then, invoke IdentityMappingService's Get with the externalId.
    // Make sure Get is invoked with the second parameter ('createMissingMapping') set to true. This will create a new mapping if no existing mapping is found
    MappedIdentity mappedContent = IdentityMappingService.Service.Get(externalId, true);
    attendee.ContentLink = mappedContent.ContentLink;
    attendee.ContentGuid = mappedContent.ContentGuid;
 
    // and then the properties from the person objects
    attendee.Title = person.Title;
    attendee.Name = person.Name;
    attendee.Company = person.Company;
    attendee.Email = person.Email;
 
    // make the content read only
    attendee.MakeReadOnly();
    return attendee;
}
protected Injected<IdentityMappingService> IdentityMappingService { get; set; }
                  

 

The Provider

With the convertion of Person objects in place, the content provider can be built. To create a provider, create a new class and inherit from ContentProvider.  There are many methods that can be overridden in order to implement the content provider of your dreams functionality that is required for your provider. Below I’ve implemented LoadContent, which will be invoked whenever loading content from the provider. This is an abstract method and requires implementation. I’ve also implemented LoadChildrenReferencesAndTypes which is invoked whenever children should be listed from a node, such as when opening a node in edit mode or when GetChildren is invoked from an IContentRepository.

public class AttendeeProvider : ContentProvider
{
    public const string Key = "attendees";
    private List<Attendee> _attendees = new List<Attendee>();
 
    // This will be invoked when trying to load a single attendee from the providers. Such as when displaying the attendee on a page or in edit mode.
    protected override IContent LoadContent(ContentReference contentLink, ILanguageSelector languageSelector)
    {
        // In order to return the attendee, the contentLink must be mapped to an e-mail so that the person object can be found using the PersonService
        MappedIdentity mappedIdentity = IdentityMappingService.Service.Get(contentLink);
 
        // the email is found in the ExternalIdentifier that was created earlier. Note that Segments[1] is used due to the fact that the ExternalIdentifier is of type Uri.
        // It contains two segments. Segments[0] contains the content provider key, and Segments[1] contains the unique path, which is the e-mail in this case.
        string email = mappedIdentity.ExternalIdentifier.Segments[1];
        return ConvertToAttendee(PersonService.GetPersonByEmail(email));
    }
 
 
    // this will pass back content reference for all the children for a specific node. In this case, it will be a flat structure,
    // so this will only be loaded with the provider's EntryPoint set as the contentLink
    protected override IList<GetChildrenReferenceResult> LoadChildrenReferencesAndTypes(
        ContentReference contentLink, string languageID, out bool languageSpecific)
    {
        // the attendees are not language specific, so this is ignored.
        languageSpecific = false;
 
        // get all Person objects
        var people = PersonService.GetAll();
 
        // create and return GetChildrenReferenceResults. The ContentReference (ContentLink) is fetched using the IdentityMapingService.
        return people.Select(p =>
            new GetChildrenReferenceResult()
            {
                ContentLink =
                    IdentityMappingService.Service.Get(MappedIdentity.ConstructExternalIdentifier(ProviderKey,
                        p.Email)).ContentLink,
                ModelType = typeof (Attendee)
            }).ToList();
    }
}

The provider automatically caches items, meaning that LoadContent will not be invoked for every request. The cache settings can be overridden, so you can control this yourself if needed.

 

Register the provider

The registration of the provider is done with an initializable module. Here is how the initialization is implemented:

public void Initialize(InitializationEngine context)
{  
    var attendeeProvider = new AttendeeProvider();
 
    // add configuration settings for entry point and capabilites
    var providerValues = new NameValueCollection();
    providerValues.Add(ContentProviderElement.EntryPointString, AttendeeProvider.GetEntryPoint("attendees").ContentLink.ToString());
    providerValues.Add(ContentProviderElement.CapabilitiesString, "Create,Edit,Delete,Search");
 
    // initialize and register the provider
    attendeeProvider.Initialize(AttendeeProvider.Key, providerValues);
    var providerManager = context.Locate.Advanced.GetInstance<IContentProviderManager>();
    providerManager.ProviderMap.AddProvider(attendeeProvider);
}

The provider's entry point is set and capabilities configured.
When working with content providers, the entry point must be a content node without any children.  The GetEntryPoint method takes care of this by creating a folder with the given name beneath the root node:

public static ContentFolder GetEntryPoint(string name)
{
    var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
    var folder = contentRepository.GetBySegment(ContentReference.RootPage, name, LanguageSelector.AutoDetect()) as ContentFolder;
    if (folder == null)
    {
        folder = contentRepository.GetDefault<ContentFolder>(ContentReference.RootPage);
        folder.Name = name;
        contentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess);
    }
    return folder;
}

Tip: Use GetBySegment to find a child node with a matching name. Performance wise this is better than invoking GetChildren and looping through each child for a possible match.

An easy way to check if the content is being loaded at the correct location is to use the “Set Access Rights” admin plugin:

attendee_accessrights 

Display the content in the UI

I want to make the attendees appear in a new tab in the assets pane. In order to do this, two things are needed: a content repository descriptor and a component.
The former is used to describe the attendee repository, which will be used by the component.

[ServiceConfiguration(typeof(IContentRepositoryDescriptor))]
public class AttendeeRepositoryDescriptor : ContentRepositoryDescriptorBase
{
    protected Injected<IContentProviderManager> ContentProviderManager { get; set; }  
    public override string Key { get { return AttendeeProvider.Key; } }
 
    public override string Name { get { return "Attendees"; } }
 
    public override IEnumerable<ContentReference> Roots { get { return new[] { ContentProviderManager.Service.GetProvider(AttendeeProvider.Key).EntryPoint }; } }
 
    public override IEnumerable<Type> ContainedTypes { get { return new[] { typeof(Attendee) }; } }
 
    public override IEnumerable<Type> MainNavigationTypes { get { return new[] { typeof(ContentFolder) }; } }
 
    public override IEnumerable<Type> CreatableTypes { get { return new[] { typeof(Attendee) }; } }
}
The repositry descriptor above is configured to serve the Attendee type, and the Roots property has been set to the EntryPoint in the provider.
If needed, you could return multiple types and roots, meaning that you could create repository descriptors for various items types - not just limited to a certain type.
 
To display the content in the assets pane, I have created a component. In the component, a reference to a dojo component must be defined. For this, I’ve used the built-in HierarchialList component, which is a base dojo component for listing content. If you need to create a custom one, it could be a good idea to check out the uncompressed js file. It is located at \modules\_protected\CMS\EPiServer.Cms.Shell.UI.zip\7.X\ClientResources\epi-cms\widget\HierarchicalList.js.uncompressed.js.
The title and description has been hard coded in this example. In order to use localization files, use the LanguagePath property.
 
[Component]
public class AttendeeComponent : ComponentDefinitionBase
{
    public AttendeeComponent(): base("epi-cms.widget.HierarchicalList")
    {
        Categories = new string[] { "content" };
        Title = "Attendees";
        Description = "All the attendees at the techforum! Displayed neatly in the assets pane";
        SortOrder = 1000;
        PlugInAreas = new[] { PlugInArea.AssetsDefaultGroup };
        Settings.Add(new Setting("repositoryKey", AttendeeProvider.Key));
    }
}

Tip: When working with components, you might run into an issue where new components are not being displayed. This could be a caching issue. Using the reset views button is a quick way to fix this. You’ll find it on the My Settings page, beneath the display option tab.

Now we can finally see some data in the UI. A simple view will make it render nicely on the site as well when dragged into a content area.

attendee_overview

We should be able to write some data to the provider as well. This is done in Content Providers 101 Part II: From read-only to writeable

Nov 14, 2014

Comments

Please login to comment.
Latest blogs
Optimizely Configured Commerce and Spire CMS - Figuring out Handlers

I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially...

Ritu Madan | Mar 12, 2025

Another console app for calling the Optimizely CMS REST API

Introducing a Spectre.Console.Cli app for exploring an Optimizely SaaS CMS instance and to source code control definitions.

Johan Kronberg | Mar 11, 2025 |

Extending UrlResolver to Generate Lowercase Links in Optimizely CMS 12

When working with Optimizely CMS 12, URL consistency is crucial for SEO and usability. By default, Optimizely does not enforce lowercase URLs, whic...

Santiago Morla | Mar 7, 2025 |

Optimizing Experiences with Optimizely: Custom Audience Criteria for Mobile Visitors

In today’s mobile-first world, delivering personalized experiences to visitors using mobile devices is crucial for maximizing engagement and...

Nenad Nicevski | Mar 5, 2025 |

Unable to view Optimizely Forms submissions when some values are too long

I discovered a form where the form submissions could not be viewed in the Optimizely UI, only downloaded. Learn how to fix the issue.

Tomas Hensrud Gulla | Mar 4, 2025 |

CMS 12 DXP Migrations - Time Zones

When it comes to migrating a project from CMS 11 and .NET Framework on the DXP to CMS 12 and .NET Core one thing you need to be aware of is the...

Scott Reed | Mar 4, 2025