Resolving an URL without {node} or {action}
For a website we’re creating I had to create a route for customers who would sign in for the first time. This URL had to be simple and should contain a unique key for us to know which user was accessing the page. How and with what we created this key is not important for now.
The URL we wanted to use was https://www.website.com/welcome/{unique code}.
I tried this with the default code to register a content route:
routes.MapContentRoute(
"welcome_page",
"welcome/{id}",
new { node = "home", action = "index" } );
This did not work and I submitted a forum post. The solution presented did not work for me the way I wanted. I figured out that routing was not working for me at all so I posted another forum post. Again Johan Bjornfot to the rescue. But my problem was not solved.
In my opinion the routing above should work. If a segment is not defined in the URL but it is defined in the defaults the segment still should be set. This means that in the example above the URL is /welcome/FOOBAR. My defaults are home as node and index as action. When my URL is resolved I think the node and action should be in the RouteData.
To help you understand I created this code snippet.
var a = new RouteData(); a.DataTokens.Add("node", "home"); a.Values.Add("action", "index"); a.Values.Add("id", "FOOBAR");
The node should be in the DataTokens because it is not a default routing segment. The action and id should be in the Values because these are default MVC routing segments.
When this would happen my welcome/FOOBAR URL would be accessible because it is being processed correctly by the MultiplexingRouteHandler which is used by default when you map a route with MapContentRoute. Unfortunately this is not how it works.
Resolving the node happens in the GetRoutedData method of the MultiplexingRouteHandler. When the DataTokens does not contain a node object this line results in a exception.
ContentReference contentLink = RequestContextExtension.GetContentLink(requestContext);
The solution I found (and maybe there are better solutions and if so please tell me!) is the following:
I created a class which inherits from MultiplexingRouteHandler. I specifically inherited from this class and not from IRouteHandler because I want everything to be the same as the default routing except for a few things and I did not want to invent the wheel all over again.
This class has a lot of private methods but GetHttpHandler and GetRouteHandler are public. The GetRouteHandler method is the one we want to alter before the URL is resolved.
public override IRouteHandler GetRouteHandler(RequestContext requestContext) {
ContentRoute r = requestContext.RouteData.Route as ContentRoute;
if (r == null) return base.GetRouteHandler(requestContext);
foreach (var i in r.Defaults)
{
if (i.Key.Equals(RoutingConstants.NodeKey))
{
if (requestContext.RouteData.DataTokens.ContainsKey(i.Key)) continue;
if (i.Value == null || String.IsNullOrWhiteSpace(i.Value.ToString())) continue;
requestContext.RouteData.DataTokens.Add(i.Key, this.GetPageReference(i.Value.ToString()));
}
if (i.Key.Equals(RoutingConstants.ActionKey))
{
if (requestContext.RouteData.Values.ContainsKey(i.Key)) continue;
requestContext.RouteData.Values.Add(i.Key, i.Value);
}
}
return base.GetRouteHandler(requestContext); }
So what does this code do? As you can see, before the MultiplexingRouteHandler.GetRouteHandler default functionality is triggered I am altering the RouteData object in my RequestContext object.
In my case I just wanted the {node} and {action} to be in my RouteData object (because RoutingContants are not constants I couldn’t use a switch statement). Casting my Route object in RequestContext to a ContentRoute gives me the opportunity to access the Defaults which are defined in my route map.
Background information
MapContentRoute is a extension method in RouteCollectionExtensions. This method adds a ContentRoute object to the RouteCollection.
If the node key is not defined in my DataTokens, I want to add it. Unfortunately EPiServer requires the node DateToken to be a PageReference and all I got is a string of the wanted node. Therefore I wrote the GetPageReference method (again, this is the way I thought was best but if you have a better solution, please comment).
private PageReference GetPageReference(string nodeValue) {
PageData page = this.contentLoader.GetBySegment(ContentReference.StartPage, nodeValue, ServiceLocator.Current.GetInstance<ILanguageSelector>()) as PageData;
return page == null ? null : page.PageLink; }
This method retrieves all children under the start page and retrieves the first page that has the correct name and returns the PageLink as PageReference.
The action should be placed in the Values object, if not present, because this is a default MVC route segment. You could alter or extend the implementation of the GetRouteHandler method if needed.
Now when I add a content route mapping in my RouteConfig I should change my RouteHandler. This can be done via the MapContentRouteParamters.
routes.MapContentRoute(
"welcome_page",
"welcome/{id}",
new { node = "home", action = "index" },
new MapContentRouteParameters
{
RouteHandler = new CustomRouteHandler(
ServiceLocator.Current.GetInstance<IContentLoader>(),
ServiceLocator.Current.GetInstance<IPermanentLinkMapper>(),
ServiceLocator.Current.GetInstance<TemplateResolver>(),
ServiceLocator.Current.GetInstance<LanguageSelectorFactory>(),
ServiceLocator.Current.GetInstance<IUpdateCurrentLanguage>()
)
} );
Now when I enter /welcome/FOOBAR in my browser, the node “home” is being resolved and my “index” action is being hit in my HomePageController.
Hope this will help someone in the future. If you have some questions or comments about the information above please leave a message.
Comments