How To Set-Up A Partial Router

In today’s tutorial, I will show you how to implement a partial router within EpiSever. If you want to know what a partial router is, then first please read Dummy’s Guide To Explaining Partial Routing Within Your EPiServer. We will do this by implementing a basic blog. The blog will be populated by a blog home page. To make life interesting, in the editors page tree, the blog pages will not live directly on the blogs home page, but will live on a blog section outside of the main web tree. Every blog will be associated with a category. For SEO reasons we want the URL for anyone to access a blog page to be in the following format

www.website.com/bloghome/category/blogpage

Partial_router_blog_page

When we visit the ‘jonsblog’ page, we will see the blog home page view:

Partial_Routing_Episerver_Bloghome

When we visit the ‘development’ page, we will see the category page view:

Partial_Routing_Episerver_CategoryView

When we visit the ‘blog-page-1’ we will see the blog page view:

Partial_Routing_Episerver_blogpage

I know what you’re thinking… wowed by the graphics of my blog. If you need me for design work, use the contact form on the side 🙂 It also means I can spin out another blog post from this 🙂

Finally we’ll look at the Episerver page tree for the site, which will be structured like this:

Partial_Routing_Episerver_Pagetree

As we want the final URL to a page to be ‘blog home -> category -> blog page’ we will need a way to route any requests to the blog home, grab anything in the URL to the right of it, like ‘category’ or ‘blog page’ and then re-route MVC to a different controller and subsequently a different view.

The Code

The first thing we need to do is define our page types:

Blog Home

[ContentType(
DisplayName = "Blog Home Page",
GUID = "858b76f0-e295-4f48-995d-19da8f47d3c0",
Description = "Blog Home Page",
GroupName = "Blog")]
public class BlogHomePage : PageData
{
}

Blog Page

[ContentType(
DisplayName = "Blog Page",
GUID = "374441cd-8412-4cea-9ff4-0e3e73c6988e",
Description = "Blog Page",
GroupName = "Blog")]
public class BlogPage : GlobalBasePage
{
[Display(Name = "Blog Category",
GroupName = SystemTabNames.Content,
Order = 100)]
[SelectOne(SelectionFactoryType = typeof(CategoriesFactory))]
public virtual string BlogCategory { get; set; }
[Display(
Name = "Blog Title",
Description = "Blog Title",
GroupName = SystemTabNames.Content,
Order = 150)]
[CultureSpecific]
public virtual string BlogTitle { get; set; }
[Display(
Name = "Blog Post",
Description = "Blog Post",
GroupName = SystemTabNames.Content,
Order = 200)]
public virtual XhtmlString BlogPost { get; set; }
}
}

You may notice I’m using a [SelectOne] for the blog category. I won’t cover the code here (it’s in the associated project download) but if you want to learn more about selection factories, I recommend reading How to Create a Selection Factory From a Page Type in Episerver

The selection factory will be used to select a category page type which is defined below:

[ContentType(
DisplayName = "Category Page",
GUID = "68C1BCCC-34E9-42CE-98C5-05BE3A0EBAF0",
Description = "Category Name",
GroupName = "Blog")]
public class CategoryPage : PageData
{
[Display(
Name = "Category Name",
Description = "Category Name",
GroupName = SystemTabNames.Content,
Order = 100)]
public virtual string CategoryName { get; set; }
}

Repositories

In the partial router, we will need to do things like getting the category and blog pages themselves. For good coding practice, I’m going to extract all this code into their own classes, called CategoryRepository and BlogRepository. You don’t need to do this but it will make your code a bit easier to test.

Category Repository

public class CategoryRepository
{
private IEpiserverDependencies _dependencies;
public CategoryRepository(IEpiserverDependencies dependencies)
{
_dependencies = dependencies;
}
public IEnumerable<CategoryPage> GetCategoryPages()
{
var categoryHomePage = GetCategoryRootPage();
if (categoryHomePage == null)
return null;
var pages = _dependencies.ContentRepository.GetChildren<CategoryPage>(categoryHomePage);
return pages;
}
public CategoryPage GetCategoryPageByRoute(string name, string slug)
{
return GetCategoryPages().Where(n => n.Name == name
&& n.URLSegment == slug).FirstOrDefault();
}
public ContentReference GetCategoryRootPage()
{
return _dependencies.ContentRepository.Get<CategoryRootPage>(new ContentReference(40)).ContentLink;
}
public CategoryPage GetCategoryPageByRoute(string slug)
{
return GetCategoryPages().Where(n => n.URLSegment == slug).FirstOrDefault();
}
}

Blog Repository

public class BlogRepository
{
private IEpiserverDependencies _dependencies;
public BlogRepository(IEpiserverDependencies dependencies)
{
_dependencies = dependencies;
}
public IEnumerable<BlogPage> GetBlogPages()
{
var blogHomePage = _dependencies.ContentRepository.GetChildren<BlogHomePage>(ContentReference.RootPage).FirstOrDefault();
if (blogHomePage == null)
return null;
return _dependencies.ContentRepository.GetChildren<BlogPage>(blogHomePage.ContentLink);
}
public BlogPage GetBlogPageByRoute(string slug)
{
return GetBlogPages().Where(n => n.URLSegment == slug).FirstOrDefault();
}
}

These two classes are pretty straightforward and don’t have anything to do with the routing magic. When we get a URL segment like ‘development’ or ‘blog-page-1’ then we need a way get the data we care about.

Partial Router

Now we can finally define the partial router:

public class BlogPartialRouter : IPartialRouter<BlogHomePage, CategoryPage>
{
public Injected<IContentLoader> contentLoader;
private BlogRepository _blogRepository;
private CategoryRepository _categoryRepository;
private ContentReference _blogPage;
public BlogPartialRouter(CategoryRepository categoryRepository, BlogRepository blogRepository, ContentReference blogPage)
{
_categoryRepository = categoryRepository;
_blogRepository = blogRepository;
_blogPage = blogPage;
}
public object RoutePartial(BlogHomePage blogHomePage, SegmentContext segmentContext)
{
var categoryPageSegment = TryGetCategoryPageSegment(segmentContext);
var blogPageSegment = TryGetBlogPageSegment(segmentContext);
if (!string.IsNullOrEmpty(blogPageSegment))
{
return _blogRepository.GetBlogPageByRoute(blogPageSegment.ToLower());
}
var categoryPage = _categoryRepository.GetCategoryPageByRoute(categoryPageSegment.ToLower());
if (categoryPage != null)
{
segmentContext.RoutedContentLink = categoryPage.ContentLink;
}
return categoryPage;
}
private static string TryGetBlogPageSegment(SegmentContext segmentContext)
{
var segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
var blogSegment = segment.Next;
segmentContext.RemainingPath = segment.Remaining;
return blogSegment;
}
private static string TryGetCategoryPageSegment(SegmentContext segmentContext)
{
var segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
var categorySegment = segment.Next;
segmentContext.RemainingPath = segment.Remaining;
return categorySegment;
}
public PartialRouteData GetPartialVirtualPath(CategoryPage categoryPage, string language, RouteValueDictionary routeValues, RequestContext requestContext)
{
return new PartialRouteData()
{
BasePathRoot = _blogPage,
PartialVirtualPath = String.Format("{0}/{1}/",
categoryPage.Name,
HttpUtility.UrlPathEncode(categoryPage.Name))
};
}
}

To create a partial router, we need to implement from IPartialRouter<T, T>. The first parameter is the page type that triggers the routing. So, in this example, wherever we put the blog home page type on the website, the partial router will be called. This bypasses Episervers normal routing by finding the router first instead of palming the request off to its corresponding controller.

In the constructor:

public BlogPartialRouter(CategoryRepository categoryRepository, BlogRepository blogRepository, ContentReference blogPage)
{
_categoryRepository = categoryRepository;
_blogRepository = blogRepository;
_blogPage = blogPage;
}

I’m passing in the two repositories we created. These will be used to look up the page data in the URL segments. I’m also defining the blogPage which is a link to the blog container where all the blog page will live.

If you’re implementing this, then basically pass in anything that you need to find the pages/data you care about.

The next bit is where the magic happens:

public object RoutePartial(BlogHomePage blogHomePage, SegmentContext segmentContext)
{
var categoryPageSegment = TryGetCategoryPageSegment(segmentContext);
var blogPageSegment = TryGetBlogPageSegment(segmentContext);
if (!string.IsNullOrEmpty(blogPageSegment))
{
return _blogRepository.GetBlogPageByRoute(blogPageSegment.ToLower());
}
var categoryPage = _categoryRepository.GetCategoryPageByRoute(categoryPageSegment.ToLower());
if (categoryPage != null)
{
segmentContext.RoutedContentLink = categoryPage.ContentLink;
}
return categoryPage;
}

We pass in the page type that will trigger the partial router (blog home page) and a Url segment. You can think of this like an array of the URL segments, so ‘jonblog’ will be one element, ‘development’ will be another and ‘blog-page-1’ will be the last one.

The first two lines are making checks to see what segments are available in the request

var categoryPageSegment = TryGetCategoryPageSegment(segmentContext);
var blogPageSegment = TryGetBlogPageSegment(segmentContext);

If we have a blog page segment then we need to check our blog data store to see if we have a matching page type, so we call the blog repository to check.

If we find a match great, we send matching the blog page back. MVC will h=then look for a matching controller. If we don’t, we should return null so a 404 error occurs.

If no blog page segment exists and a category page exists we pretty much follow the same process, or, using the category repository to try and find a matching category page that has the URL segment as the name. If it exists, pass it back else return null and a 404 will occur.

public PartialRouteData GetPartialVirtualPath(CategoryPage categoryPage, string language, RouteValueDictionary routeValues, RequestContext requestContext)
{
return new PartialRouteData()
{
BasePathRoot = _blogPage,
PartialVirtualPath = String.Format("{0}/{1}/",
categoryPage.Name,
HttpUtility.UrlPathEncode(categoryPage.Name))
};
}

Registering The Partial Router

To trigger the partial router we need to create an Initialization Module. If you haven’t come across one of these yet, I would recommend reading Initialization Modules Explained.

using EPiServer.Web.Routing;
[InitializableModule]
public class DataInitialization : IInitializableModule
{
Injected<IEpiserverDependencies> epiServerDependencies;
public void Initialize(InitializationEngine context)
{
var dependency = epiServerDependencies.Service;
var blogRouter = new BlogPartialRouter(new CategoryRepository(dependency), new BlogRepository(dependency), new ContentReference(39));
RouteTable.Routes.RegisterPartialRouter<BlogHomePage, CategoryPage>(blogRouter);
}
public void Uninitialize(InitializationEngine context)
{
}
public void Preload(string[] parameters)
{
}
}

This code is pretty simple we new up our custom BlogPartialRouter passing in the constructor items we defined. We then register it using Episervers RegisterPartialRouter. Note this is a custom extension method so you need to include using EPiServer.Web.Routing; namespace

Controllers & Views

Now we have a system that will re-route any matching URL segments. We need to define the controllers and views to trigger them:

Blog Home Page

public class BlogHomePageController : BasePageController<BlogHomePage>
{
public ActionResult Index(BlogHomePage currentPage)
{
var blogRepository = new BlogRepository(EpiserverDependencies);
var blogPages = blogRepository.GetBlogPages();
return View(blogPages);
}
}

The view

@model IEnumerable<JonDJones.Com.Core.Pages.BlogPage>
@{Layout = "";}
<h1>Blog Listings Page</h1>
@foreach (var blogPage in Model)
{
<div>
Blog Name : @blogPage.PageName
</div>
}

Blog Page

public class BlogPageController : BasePageController<BlogPage>, IRenderTemplate<BlogPage>
{
public ActionResult Index(BlogPage currentPage)
{
var blogPage= Request.RequestContext.GetRoutedData<BlogPage>();
return View(blogPage);
}
}

The the view

@model JonDJones.Com.Core.Pages.BlogPage
@{Layout = "";}
<div>
Page Name: @Model.PageName
</div>
<hr />
<div>
Tags: @Model.BlogCategory
</div>
<hr />
<div>
@Model.BlogPost
</div>

Category Page

public class CategoryPageController : BasePageController<CategoryPage>, IRenderTemplate<CategoryPage>
{
public ActionResult Index(CategoryPage currentPage)
{
var categoryPage = Request.RequestContext.GetRoutedData<CategoryPage>();
return View(categoryPage);
}
}

The the view

@model JonDJones.Com.Core.Pages.CategoryPage
@{Layout = "";}
Welcome to the category page @Model.CategoryName

Conclusion

Well, it takes a shit load of code but we finally got there! If you have followed the steps (use the working project sample from my GitHub if you’re having issues) you should have a basic blog working that does partial routing for two different page types.

We basically defined three-page types, one to trigger off our customer routing and that defined the pages. Any of the Url Segments to the right of the blog home page bit in the Url ,we try to match up to pages that exist within Episerver.

If we have a match, we pass that page type back and MVC then triggers a matching controller and renders what we need. If we don’t get a match a 404 page is displayed.

Code Sample

All the above code can be downloaded in a fully working website from my GitHub account here.

JonDJones.com.EpiserverPartialRouting

Jon D Jones

Software Architect, Programmer and Technologist Jon Jones is founder and CEO of London-based tech firm Digital Prompt. He has been working in the field for nearly a decade, specializing in new technologies and technical solution research in the web business. A passionate blogger by heart , speaker & consultant from England.. always on the hunt for the next challenge

More Posts

2 replies
  1. Sven
    Sven says:

    You might want to change name of property “newsContent” to blogPage in the Blog page controller. (I guess you started with EpiServers example)
    Cheers!

    Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *