Allowing web editors to apply PageType based filtering on the EPiServer edit mode PageTree

When you have an EPiServer installation containing thousands of different pages built up by far-too-many page types, locating pages of just one of them may turn out to be just a little too time consuming. Since I have grown to like the adaptive control approach more and more lately, I decided to create a filtering mechanism for cleaning out dead ends in the EPiServer page tree based on this notion. Source code, droppable binaries, GitHub links and whatnot is found at the bottom.

Note: I did some refactoring after a review session with Stefan Forsberg (Thanks Stefan!), so the snippets in this post may not be entirely accurate. The overall paradigm is the same however. The 90’s also wanted its source code zip back, so I settled for the droppable binary, GitHub and a NuGet package.

The page tree filter control showing all pages in the EPiServer edit mode page treeThe page tree filter control showing only pages of standard page type in the EPiServer edit mode page tree

The code adds a new DropDownList control at the top of the page tree, which in turn is populated with all of the available EPiServer page types in an index based order. Selecting one of them triggers a postback which rebinds the tree removing all unwanted pages. As you can see in the image above, some pages are bold and some are not; this is to help the user separating the actually selected pages from the ones just being above them in the tree keeping the structure intact. In other words, selecting the page type [AlloyTech] Standard page will give both the bolded standard page Resellers as well as the parent [AlloyTech] Listing page How to buy, and so on all the way up to the Root folder.

As you may suspect, the bold EPiServer shortcut pages in the image are both of the selected type; their boldiness has nothing to do with where the shortcut is in fact pointing. Since this is an adaptive control based solution, and control adapters tend to affect all instances of an original control, this filtering functionality will also be available for properties employing a page selector popup window; such as a PropertyLinkCollection or a PropertyPageReference. Furthermore, if you ever use the Favorites tab, you will find that the filter works here as well.

EPiServer PageTree PageType filter applied on a page selector popup windowEPiServer PageTree PageType filter being applied to the Favorites tab in edit mode

How control adapters help with filtering pages in the EPiServer PageTree

The filter functionality consists of two different control adapters; one responsible for adding the PageType selector drop down, the other performing the actual filtering. Use of these may be configured in the App_Browser directory; this post contains more information about configuring the usage of control adapters. However, unless you are doing something special it will probably be enough just to use the included mappings from the browser file inside the zip archive.

AdapterMappings.browser

<browsers>
 <browser refID="Default">
  <controlAdapters>
   <adapter controlType="EPiServer.UI.Edit.PageExplorer"
            adapterType="EPi.. ..Tree.PageExplorerAdapter" />
   <adapter controlType="EPiServer.UI.WebControls.PageTreeView"
            adapterType="EPi.. ..Tree.PageTreeViewAdapter" />
  </controlAdapters>
 </browser>
</browsers>

The PageExplorerAdapter is where the DropDownList is added; or rather the PageTypeSelector control inheriting the DropDownList class. In order for the code to be less cluttered I decided not to place the logic retrieving page types within the adapter itself, but rather make a separate control for it.

PageTypeSelector.cs

private static IEnumerable<ListItem> AllPageTypes()
{
    return PageType.List()
        .Select(pageType => new ListItem
        {
            Text = pageType.Name,
            Value = pageType.ID.ToString(CultureInfo.InvariantCulture)
        });
}

The default constructor of this control retrieves a list of all available EPiServer page types using EPiServer.DataAbstraction.PageType (24). These are mapped into more usable ListItem objects (25-29) and then attached as a DataSource to the DropDownList. Since the PageTypeSelector control is added inside of the OnInit event in the PageExplorerAdapter there is no need bothering with maintaining view or control states, as .NET will do this automatically later in the page lifecycle. For the code to be more pluggable, the CSS file link is inserted into the page header from this adapter as well; you might want to change this adding it in some more minify friendly fashion though.

The other adapter, the PageTreeViewAdapter, is where the more interesting things take place. An override of the OnLoad event allows for attaching of two new event handlers to the tree.

PageTreeViewAdapter.cs

((PageDataSource)pageTreeView.DataSource).Filter +=
    new FilterEventHandler(PageExplorerAdapter_Filter);
pageTreeView.PageTreeViewItemDataBound +=
    new PageTreeViewEventHandler(BoldifySelectedPageType_OnItemDataBound);
pageTreeView.DataBind();

The latter event handler, BoldifySelectedPageType_OnItemDataBound (61), is responsible for making the page names bold in the page tree for the proper pages. It does so simply by adding an additional CSS class to the correct link whenever a suitable page comes along. To filter the collection containing the pages, the event handler PageExplorerAdapter_Filter is attached to the PageTreeView‘s PageDataSource Filter event (59). If the event handler decides that the collection should indeed be filtered, it does so by looping through the pages removing all whose ids are not in a predetermined int array (called PagesToKeep).

 var pagesOfSelectedType = DataFactory.Instance.FindPagesWithCriteria( PageReference.RootPage, criteria);

    var parents = pagesOfSelectedType.Aggregate(Enumerable.Empty<int>(),
        (current, page) => current.Union( DataFactory.Instance.GetParents(page.PageLink).Select(r=>r.ID)))
        .ToList();
    parents.Add(PageReference.RootPage.ID);

    return parents.Union(pagesOfSelectedType.Select(p=>p.PageLink.ID));
}

The PagesToKeep array is constructed in the private PathsToSelectedPages method further down in the adapter class. First a criterion is set up and a basic FindPagesWithCriteria call is used to retrieve all pages with the correct PageTypeID (108). This is followed by a union (115) with a distinct list containing all of the page’s parents (110-113), in order to maintain the PageTree structure all the way to the root node.

The tricky part writing this filtering functionality was determining which PageType that the user wanted to filter on, as there may be multiple instances of the PageExplorer and PageTreeView controls on the same page; as for instance the ones residing on the Structure and the Favorites tabs. To solve this I wrote the two short control extension methods located in the ControlExtensions.cs file.

_selectedPageType =
    ((PageTypeSelector)CurrentPageExplorer. FindControlRecursively("PageTypeSelector"))
    .SelectedValue;
private PageExplorer _currentPageExplorer;
private PageExplorer CurrentPageExplorer
{
    get { return _currentPageExplorer ?? (_currentPageExplorer = Control.FindParentControlOfType<PageExplorer>()); }
}

As I knew that the PageExplorer control was a parent to the PageTreeView, and that I had placed the PageTypeSelector drop down within the PageExplorerAdapter, I decided to start from the inside going upward rather than trying to find the correct needle in the haystack from another direction.

The first extension method recursively looks for parent controls of a given type (35). In this case I wanted to find the closest PageExplorer control as it was the one being connected to the tree that I wanted to filter. After finding it I would recursively take myself back down again until I found the PageTypeSelector that I was looking for (25). As all of the involved controls are placed in such close proximity to one another, I doubt that the recursion will have any noticable effect on performance as opposed to trying to find the control by an id starting from somewhere else.

I have tried this at work in the project that I am currently working on. It has a giant code base and consists of approximately 22 000 EPiServer pages made up by 77 different page types. I thought it might cause a problem, but I was unable to see any noticable performance loss adding this functionality. It would be really interesting to test this on an even larger page quantity, and find out when I would need to implement some sort of caching.

Update: The PageTreeViewAdapter in Admin Mode

The Set Access Rights functionality in the EPiServer admin mode also uses the PageTreeView control to render its page tree. It does so without enclosing it inside a PageExplorer, and since the PageType filter assumes both controls to be present, as it uses the parent one for the PageType list, I decided to prevent it from being render here. Even though it may feel like a loss, I believe it to be a good thing as filtering could result in confusion for the user as they inherit access rights downward into the structure; it may not be totally clear to them which pages will have their rights changed if a filter is applied.

The Set Access Rights functionality in EPiServer admin mode without the PageType filter

Update: New functionality

In version 1.1.3.0 new functionality have been added allowing user specific and global selection of the page types that should be available in the DropDownList. As the global information is persisted using the EPiServer Dynamic Data Store, it will not work for earlier versions of EPiServer CMS. You may read more about this functionality in this post.

Source code

The droppable binary version 1.1.2.0 is no longer the latest one, but it is the last one which do not make use of the EPiServer Dynamic Data Store.

Droppable binary: PageTypeTreeFilter_binary_1.1.2.0.zip
GitHub link: https://github.com/matkun/Blog/tree/master/PageTypeTreeFilter
NuGet package: PageTypeTreeFilter at the EPiServer NuGet feed.

14 Comments

  1. Mathias Kunto February 6, 2012
    • Mathias Kunto February 6, 2012
  2. Ted Nyberg February 6, 2012
    • Mathias Kunto February 6, 2012
  3. valdis.iljuconoks February 7, 2012
  4. Joel Abrahamsson February 7, 2012
  5. Mathias Kunto February 9, 2012
  6. Stephan Kvart February 14, 2012
    • Mathias Kunto February 15, 2012
    • Mathias Kunto March 4, 2012
  7. tepu December 18, 2013
    • Mathias Kunto December 26, 2013
  8. Valdis Iljuconoks April 21, 2015
    • Mathias Kunto April 25, 2015