Niklas Melinder
Nov 14, 2010
(1 votes)

EPiSocial – Using PageObjects

After my previous post on the EPiSocial plugin, Paul Smith pointed out that I should consider using Page Objects (PO) rather than plain Dynamic Data Store (DDS) to store page related data. The upside of using PO is that it provides support for mirroring and import/export. The downside being that it handles things on a page by page basis which makes queries over multiple pages a bit tricky. This is necessary when doing things like getting pages with the highest rating or getting pages with most comments.

Luckily, the process of changing the plugin from using DDS to PO is as simple as creating new implementations of the ICommentRepository and IRatingRepository interfaces.

After some minor changes from the previous version they look like this:

   1: public interface ICommentRepository
   2:     {
   3:         void AddComment(PageComment pageComment);   
   4:         void RemoveComment(string commentId);
   5:         void RemoveAllCommentsForPage(int pageId);
   6:         IEnumerable<PageComment> GetCommentsForPage(int pageId, string pageLanguage);
   7:         IEnumerable<PageComment> GetLatestComments(int count, string pageLanguage);
   8:         IEnumerable<int> GetMostCommentedPageIds(int count, string pageLanguage);
   9:     }
   1: public interface IRatingRepository
   2:     {
   3:         void AddRating(PageRating pageRating);
   4:         void RemoveAllRatingsForPage(int pageId);
   5:         double GetAverageRatingForPage(int pageId, string pageLanguage);
   6:         int GetNumberOfRatingsForPage(int pageId, string pageLanguage);
   7:         IEnumerable<int> GetHighestRatedPageIds(int count, string pageLanguage);        
   8:     }

In this post I will focus on the PO implementation of ICommentRepository.

Page Objects

The first thing I noted was that the documentation for PO was rather slim. Nothing in the SDK, all I found was a single Tech Note that described the features briefly.

One thing that differs when using PO instead of DDS is that a PO is stored as a single object per page. To be able to store multiple comments for a single page I created a container object to hold the comments.

   1: public class PageObjectCommentContainer : IDynamicData
   2:     {
   3:         public Identity Id { get; set; }
   4:         public List<DynamicDataStoreComment> Comments { get; set; }
   6:         public PageObjectCommentContainer()
   7:         {
   8:             this.Comments = new List<DynamicDataStoreComment>();
   9:         }
  10:     }


The PageObjectCommentRepository takes two dependencies. The first being a CommentMapper that handles the mapping described below. The second one is a PageObjectManagerFactory that handles creation of PageObjectManager objects based on page id.

   1: private const string CommentKey = "PageComments";
   2: private readonly CommentMapper commentMapper;
   3: private readonly PageObjectManagerFactory pageObjectManagerFactory;
   4: private readonly DynamicDataStore commentStore;
   6: public PageObjectCommentRepository(CommentMapper commentMapper, PageObjectManagerFactory pageObjectManagerFactory)
   7:         {
   8:             this.commentMapper = commentMapper;
   9:             this.pageObjectManagerFactory = pageObjectManagerFactory;
  10:             this.commentStore = DynamicDataStoreFactory.Instance.GetStore(typeof(DynamicDataStoreComment));
  11:         }

About mapping

Before saving comments to the DDS the PageComment object is mapped to a DynamicDataStoreComment object that implements the necessary IDynamicData interface. The reverse thing happens when getting comments from the store. This mapping might seem unnecessary, why don´t use the DynamicDataStoreComment directly instead? There are good reasons.

One being that I want the plugin to be extendable. If I or someone else wanted to create a different implementation using a different storage, that implementation probably won´t care about EPiServer.Data.

Another reason is that testing code using the API should not have to know about EPiServer.Data either.


   1: public void AddComment(PageComment pageComment)
   2:         {
   3:             var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(pageComment.PageId, pageComment.PageLanguage);
   4:             var ddsComment = this.commentMapper.MapToDynamicDataStoreComment(pageComment);
   5:             var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
   7:             if (poCommentContainer == null)
   8:             {
   9:                 poCommentContainer = new PageObjectCommentContainer();                
  10:             }
  12:             poCommentContainer.Comments.Add(ddsComment);
  13:             pageObjectManager.Save(CommentKey, poCommentContainer);               
  14:         }


When removing a comment we first have to load the object, remove the comment from the list and save it back again. I also found that I had to delete the comment directly from the DDS for it to disappear completely, otherwise it will still be around when making queries like getting the latest comments below.

   1: public void RemoveComment(string commentId)
   2:         {
   3:             Identity identity;
   5:             if (!Identity.TryParse(commentId, out identity))
   6:             {
   7:                 throw new InvalidOperationException("Comment id is not an Identity.");
   8:             }
  10:             var ddsComment = commentStore.Load<DynamicDataStoreComment>(identity);
  11:             var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(ddsComment.PageId, ddsComment.PageLanguage);
  12:             var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
  14:             if (poCommentContainer == null)
  15:             {
  16:                 return;
  17:             }
  19:             poCommentContainer.Comments
  20:                 .RemoveAll(c => c.Id == identity);
  22:             pageObjectManager.Save(CommentKey, poCommentContainer);
  23:             this.commentStore.Delete(identity);
  24:         }


   1: public void RemoveAllCommentsForPage(int pageId)
   2:        {
   3:            var pageObjectManager = pageObjectManagerFactory.GetPageObjectManagerForPage(pageId);
   4:            pageObjectManager.Delete(CommentKey);
   5:        }


   1: public IEnumerable<PageComment> GetCommentsForPage(int pageId, string pageLanguage)
   2:         {
   3:             var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(pageId, pageLanguage);
   4:             var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
   6:             if (poCommentContainer == null)
   7:             {
   8:                 return new List<PageComment>();
   9:             }
  11:             return poCommentContainer.Comments
  12:                 .Select(c => this.commentMapper.MapToPageComment(c));
  13:         }


So far it´s been pretty straight forward. But since the PageObjectManager works on a per page basis, this kind of query cannot be done using it. But since PO uses DDS in the background, we can create a query using DDS directly to get the latest comments:

   1: public IEnumerable<PageComment> GetLatestComments(int count, string pageLanguage)
   2:         {
   3:             return this.commentStore.Items<DynamicDataStoreComment>()
   4:                 .Where(c => c.PageLanguage == pageLanguage)
   5:                 .OrderByDescending(c => c.Created)
   6:                 .Select(c => commentMapper.MapToPageComment(c))
   7:                 .Take(count);
   8:         }


The same thing applies here, we have to use DDS directly to get what we are looking for.

   1: public IEnumerable<int> GetMostCommentedPageIds(int count, string pageLanguage)
   2:         {
   3:             return this.commentStore.Items<DynamicDataStoreComment>()
   4:                 .Where(c => c.PageLanguage == pageLanguage)
   5:                 .GroupBy(c => c.PageId)
   6:                 .OrderByDescending(c => c.Count())
   7:                 .Select(c => c.Key)
   8:                 .ToList()
   9:                 .Take(count);
  10:         }

Unfortunately, due to a bug in the DDS implementation we cannot call Take() directly but have to call ToList() first. This is not optimal and I´m hoping this is fixed in the next release of EPiServer.

Wire it up

Now that we have our new PO repository ready we have to tell the application that we wish to use it instead of the default DDS implementation.

EPiSocial uses StructureMap to wire up it´s dependencies. To override the default setup we have to add a StructureMap registry of our own and the plugin will automatically pick it up during it´s initialization. So we add the following registry to the web application:
   1: public class PageObjectRegistry : Registry
   2:     {
   3:         public PageObjectRegistry()
   4:         {
   5:             For<ICommentRepository>()
   6:                 .Use<PageObjectCommentRepository>();
   8:             For<IRatingRepository>()
   9:                 .Use<PageObjectRatingRepository>();
  10:         }
  11:     }

For more on StructureMap check out this post by Stefan.

That´s all, we are now ready to use the PO version of the plugin.

An updated version of the plugin can be downloaded here. It now contains a EPiSocial.PageObjects assembly with the new PO implementation.

Nov 14, 2010


Please login to comment.
Latest blogs
How to fix scheduled job 'Remove Abandoned BLOBs' if it keeps failing

Optimizely inlcudes a job named 'Remove Abandoned BLOBs'. This post will help you fix it if it's no longer working.

Henning Sjørbotten | Sep 26, 2023 | Syndicated blog

Optimizely Web Experimentation Metrics

Within the domain of Optimizely Web Experimentation Metrics, the emphasis is on objective key performance indicators (KPIs) selected to assess an...

Matthew Dunn | Sep 23, 2023 | Syndicated blog

Optimizely For you Intranet

Having been at Optimizely and in the CMS industry for nearly 16 years I have seen clients’ intranet requirements go from a simple site just to hous...

Robert Folan | Sep 22, 2023

Vulnerability in EPiServer.GoogleAnalytics v3 and v4

Introduction A potential security vulnerability was detected for Optimizely Google Analytics addon (including EPiServer.GoogleAnalytics and...

Bien Nguyen | Sep 20, 2023

Overriding Optimizely’s Content Recommendations Block to Implement Custom Recommendations

Introduction The Content Recommendations add-on for Optimizely CMS dynamically recommends content from your site tailored to the interests of each...

abritt | Sep 13, 2023 | Syndicated blog

Developer contest! Install the AI Assistant and Win Bose QC45 Headphones!

We are thrilled to announce a developer contest where you have the chance to win a pair of Bose Headphones. The goal is to be the first developer t...

Luc Gosso (MVP) | Sep 7, 2023 | Syndicated blog