Dynamic Routing with Kentico MVC - Service Pack (Obsolete)
This article is obsolete.
Just want the code? Just check out my Kentico MVC Boilerplate project on GitHub.
You can also check out the quick summary at the bottom.
What is Dynamic Routing?
MVC Routing operates on the basis that a request is handled by Controllers and Actions in predefined routes. Dynamic Routing is where a request maps to a Page (TreeNode), and that Page's settings (Template, Page Type) determines how the request should be handled (which Controller/Action/View). In Portal Engine, the Url matched the NodeAliasPath or Url Aliases of a page, and the Page's Template then determined how it was handled. In this article we will outline how to restore the Portal Engine request handling now that the Kentico 12Service Pack is out.
What was missing that Service Pack provided?
While I wrote about this topic in January, it was not a complete solution yet. Two vital pieces were missing that Kentico's Service Pack finally provided to make the Dynamic Routing capabilities with Kentico 12 complete, and those would be Alternative URLs and MVC Templates.
Alternative URLs
Alternative URLs provide the means to assign different URLs to the same page, which is important because many times SEO optimized URLs do not match a specific MVC Route.
You can enable and configure this feature in the Settings → URLs and SEO within your Kentico admin. You must check "Enable editing interface" to allow users to add these URLs through the Properties → Alternative URLs tab when you select your page on the Content Tree. You also need to register it in your MVC Application.
Alternative URLs have 2 different modes, Redirect or Rewrite. The Redirect (default) mode will simply direct the given alternative URLs to the main URL as determined by it's Page Type Presentation URL setting. Rewrite will allow that page to render with that URL showing in the user's address (to the server however it receives the request as if it was the normal page, not the Alternative URL).
Once enabled, you can add the Alternative URLs to your pages. Keep in mind, these alternative URLs are language specific, switching language will give you a unique list of alternative URLs for the page.
MVC Templates
MVC templates are the next piece, which allow you to create "Templates" (really a View and ViewModel with optional Custom Controller), and select a template on your pages. This also includes optional Properties that can be passed to your View, which is a nice bonus.
The basic Page Template is just an attribute tag that details what View the page should be rendered with, such as this:
[assembly: RegisterPageTemplate("Home.Template1", "Template 1", customViewName: "_HomeTemplate1")]
This feature makes it possible for our Dynamic Routing to automatically render the page with the right View/Controller without needing to worry about special MVC Routing. Your Controller that handles the route must leverage the code return new TemplateResult(FoundNode.DocumentID);
to produce the proper result for the given page (DocumentID).
Dynamic Routing – The Concept and Code
While this article is an update of the existing January article, and some of the content may seem very familiar, the code and descriptions have been modified, so I would re-read the sections so you don't miss anything.
Also, all of the code files for this can be found on the Kentico Boilerplate, and consist of the following files:
- App_Start/RouteConfig.cs
- Controllers/PageTemplates/DynamicPageTepmlateController.cs
- Controllers/PageTemplates/EmptyPageTemplate.cs
- Controllers/PageTemplates/EmptyPageTemplateFilter.cs
- Library/Dynamic Route Handling (All files)
- Library/Helpers/DocumentQueryHelper.cs
- Library/Helpers/EnvironmentHelper.cs
Refresher: How Portal Finds and Renders a Page
Let's take a quick refresh of how Portal Method displays a page. When a page request comes in, Kentico matches that request to a Page, through the NodeAliasPath or Url Aliases. Once it finds the page, it then looks at the Page's Template and proceeds to render the page (using things such as the Template's Master Page/Nesting, widget zones, web parts, etc).
Refresher: How MVC Finds and Renders a Page (Non-Dynamically)
MVC is quite different. In an MVC Site, you define Routes on startup. When a page request comes in, it matches it to a predefined Route. That route then determines what Controller and Action is called. This Controller/Action gathers the information it needs, and passes that to a View which then renders the page.
Here's a good example. Say Fred wants a blog on the site. He adds a Blog type page under the root in Kentico, with a path of /FredsBlog, and then adds some articles under it. You create a route of /FredsBlog/{ArticleName} that uses the controller "BlogController."
However now Sally wants a blog as well, so she adds a blog at /SallysBlog. Now you need to add another route of /SallysBlog/{ArticleName} with the same BlogController in use.
To further complicate things, within Kentico you need to define a Url Pattern so Kentico knows what path to call on your MVC site to preview the page, and now you have two unique patterns.
Restoring Dynamic Routing
In order to fully restore Dynamic routing so the URL maps to a page which determines it's rendering, we're going to dive into a couple MVC concepts that will help us accomplish our goals. They are MVC Route Handlers, Route Constraints, and ControllerFactory.
MVC Route Handlers
When you define a route within MVC, you can also assign a Route Handler. This provides the means to perform additional logic with a route, such as adding additional routing context (like the sample Dancing Goat's MultiCultureMvcRouteHandler). This class inherits the MvcRouteHandler where you can override the GetHttpHandler
. We'll use a RouteHandler to inject custom logic to dynamically determine what Controller to render based on the Node found.
Route Constraints
MVC provides a default route of {controller}/{action}/{id}
which is leveraged heavily for things such as Html.RenderAction
and other similar requests. However, we run into two scenarios where dynamic routing may cause issues. The first scenario is we only want to handle a route Dynamically if we can find a page that the request is mapping to. The other scenario is if a page's NodeAliasPath
Matches a Controller/Action, we need to decide if the normal Controller/Action should handle the request, or if the dynamic routing should handle it.
We handle these through Route Constraints. These are logic classes that allow us to determine if a route in our RouteCollection should try to handle the request. In my code, I have 2 Constraints
This constraint will allow the Dynamic Routing route to trigger only if a Page Node can be found given the current URL. Our dynamic routing depends on finding the Node from the request URL, so if this constraint can't find the page, neither will our dynamic routing.
This constraint checks to see if the found Controller for the request has a [KMVCRouteOverPathPriority] Attributed on it, which signals that this controller should handle the request (even if a page matches the request). We add this to the {controller}/{action}/{id}
route before our Dynamic Routing route, but leave it off the {controller}/{action}/{id}
route that appears after our dynamic routing.
Here is the actual code in the RouteConfig.cs file.
Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.
ControllerFactory
A controller factory (ControllerBuilder.Current.GetControllerFactory()) is a class that allows you to get a controller purely on the name of it (making it very dynamic). This is done through the CreateController(RequestContext, ControllerName) method. The RequestContext has things such as the Request's properties (Controller, Action, and other properties the Controller needs to determine the correct method to call), and the controller name is simply that, the name of the controller (ex "Blog" for a BlogController). We'll leverage this to dynamically set our Controller and Action based on the found Page's settings.
Dynamic Routing Logic
Now let's discuss the routing logic for our KMVCDynamicHttpHandler. The overall logic is we are going to dynamically alter what the Controller and Action will be for the incoming request based on the TreeNode
found (which is based on the request URL).
I call a GetNodeByAliasPath()
helper method I created to find the TreeNode
to start. Then from it, I gather the MVC Template configuration field to see if it uses MVC Templates. I also need to do a check on the Temp_PageBuilderWidgets
table which contains a temporary MVC Template value when switching page templates without saving prior.
Next I do a switch statement on the class name. This is where if you wish, you could perform your own custom logic. The default handling though does the following:
-
If it uses MVC Templates (and that MVC Template is not the EmptyTemplate), send the request to the DynamicPageTemplateController which will return the TemplateResult for that page
-
If it doesn't use MVC Templates, try to render with a Controller that matches the PageType's code name (ex Custom.Blog page type would try to hit a Custom_BlogController controller's index page). This step is optional, but does allow you to render all Pages of a given page type with a single controller, which makes it still dynamic without need of Page Templates.
-
If that controller is not found, then send to the NotFound() action of the DynamicPageTemplateController which will trigger a 404.
The EmptyTemplate is a special "Page Template" that is added in when there is only 1 other template available from filtering. You don't need to have this in here if you don't wish, I added it since currently if you only have one Template available Kentico will automatically assign it to the page upon creation, which can lead to some confusion as you were never presented with a Select Page Template menu. My logic will present the user with the option to select a "No template" template which the dynamic route ignores.
Luckily as well, we don't need to worry about Alternative URLs, in either case (redirect or rewrite) the final request will contain the NodeAliasPath, as long as you perform the last step...
The last thing needed to make thizas all work is to tell Kentico that the NodeAliasPath is the page's URL. To do this, simply go to your Page Type, and set the URL Pattern to {% NodeAliasPath %}
. Now Kentico will use the NodeAliasPath for the actual URL of the page, and the dynamic routing will handle the rest!
Let's wrap it up
So in short, here's what we did:
-
Enable Alternative URLs in our Kentico Settings
-
Enable it in our MVC Application
-
Set the Page Type Presentation URL to {% NodeAliasPath %}
-
Add to the RouteCollection a route that runs any Controller/Action match if the Controllers has the [KMVCRouteOverPathPriority] attribute
-
Add to the RouteCollection a route that uses our KMVCDynamicRouteHandler (if a page can be found via the KMVCPageFoundConstraint)
-
Add to the RouteCollection a route that will run any Controller/Action if the first 2 routes aren't triggered already.
-
Add MVC Templates so you can assign them to your page, or add Controllers that match the Page Type's code name (replacing . with _ , ex "custom.blog" => "custom_blogController")
All code again is found on the Kentico Boilerplate, please consider Forking and send a Pull Request if there's something missing in it, or check out the Issues list in case you want to tackle some of the issues that arise. Happy Coding!