Views: 4643
Number of votes: 1
Average rating:

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.

Apr 11, 2014

Johan Björnfot
( By Johan Björnfot, 4/11/2014 10:23:03 AM)

Iin CMS 7.5 a method GetBySegment was added to IContentLoader, it should be faster than calling GetChildren (especially if there are many children).

An alternative to subclassing MultiplexingRouteHandler would be to have a custom SegmentBase implementation for {id}, what you should do in your SegmentBase implementation is basically assigning segmentContext.RoutedContentLink in your RouteDataMatch method (and consume the segment). Joel has written a god post about this in his post http://joelabrahamsson.com/custom-routing-for-episerver-content/ under section "Custom routing using a custom segment"

hans.leautaud
( By hans.leautaud, 4/11/2014 10:46:30 AM)

Johan, yes you pointed that out already but that did not work for me. The {id} is not the problem. The problem is that {node} and {action} are not in the url.

hans.leautaud
( By hans.leautaud, 4/11/2014 1:01:59 PM)

Johan, you are right about the GetBySegment method. I changed that bit.

K Khan
( By K Khan, 4/11/2014 1:03:34 PM)

I achieved routing for URLS like abc.com/category/clothes/color/red/
in this way

public class CustomPartialRouter : ICommerceRouter, IPartialRouter
{

public CustomPartialRouter(ContentReference routeStartingPoint, RootContent commerceRoot)
{
ContentRoute.RoutingContent += RoutingContent;
this.RouteStartingPoint = routeStartingPoint;
// Blah Blah
}

private void RoutingContent(object sender, RoutingEventArgs routingEventArgs)
{
//FiltersIdentifier
string commerceIdentifier = System.Configuration.ConfigurationManager.AppSettings["CommerceIdentifier"];
string fi = System.Configuration.ConfigurationManager.AppSettings["FiltersIdentifier"];

string []filtersIdentifiers = {""};
if(!string .IsNullOrEmpty(fi))
filtersIdentifiers= fi.Split('|');
var segmentContext = routingEventArgs.RoutingSegmentContext;

if(segmentContext.RemainingPath.Contains(commerceIdentifier))
{
var seg = segmentContext.GetNextValue(segmentContext.RemainingPath);
var segText = seg.Next;
//Covers SHop and en/shop
if (segText != commerceIdentifier)
{
seg = segmentContext.GetNextValue(seg.Remaining);
segText = seg.Next;
}
//category segment
var catSegment = segmentContext.GetNextValue(seg.Remaining);
var catSegmentText = catSegment.Next;
//Filters segment 1 key and value
var seg1key = segmentContext.GetNextValue(catSegment.Remaining);
var seg1keytext = seg1key.Next;
var seg1val = segmentContext.GetNextValue(seg1key.Remaining);
var seg1valtext = seg1val.Next;

for (int i = 0; i < filtersIdentifiers.Length; i++)
{
string filterid = filtersIdentifiers[i];
if (!string.IsNullOrWhiteSpace(filterid) && seg1keytext == filterid)
{
segmentContext.RouteData.Values[seg1keytext] = seg1valtext;
}
}
segmentContext.RemainingPath = catSegmentText;
}
}

public PartialRouteData GetPartialVirtualPath(CatalogContentBase content, string language, System.Web.Routing.RouteValueDictionary routeValues, System.Web.Routing.RequestContext requestContext)
{
if (!this.IsValidRoutedContent(content))
{
return null;
}
string text = null;
ISearchEngineInformation searchEngineInformation = content as ISearchEngineInformation;
if (searchEngineInformation != null)
{
text = searchEngineInformation.SeoUri;
}
if (requestContext.GetContextMode() == ContextMode.Default && !string.IsNullOrEmpty(text))
{
routeValues.Remove(RoutingConstants.NodeKey);
routeValues.Remove(RoutingConstants.LanguageKey);
return new PartialRouteData
{
BasePathRoot = ContentReference.StartPage,
PartialVirtualPath = text
};
}
System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
if (!this.TrySetVirtualPathRecursive(content, language, stringBuilder))
{
return null;
}
if (requestContext.GetContextMode() == ContextMode.Edit)
{
routeValues["id"] = content.ContentLink.ToString();
}
return new PartialRouteData
{
BasePathRoot = this.RouteStartingPoint,
PartialVirtualPath = stringBuilder.ToString()
};
}

public object RoutePartial(PageData content, EPiServer.Web.Routing.Segments.SegmentContext segmentContext)
{
Validator.ThrowIfNull("segmentContext", segmentContext);
SegmentPair nextValue = segmentContext.GetNextValue(segmentContext.RemainingPath);
if (string.IsNullOrEmpty(nextValue.Next))
{
return null;
}
return null;
}

protected virtual bool TrySetVirtualPathRecursive(CatalogContentBase content, string language, System.Text.StringBuilder virtualPath)
{
return true;
}
protected virtual bool TryInsertRouteSegment(ContentReference contentLink, string language, System.Text.StringBuilder virtualPath)
{
return true;
}
protected virtual bool IsValidRoutedContent(CatalogContentBase content)
{
return content != null;
}
}

Jeroen Stemerdink
( By Jeroen Stemerdink, 4/18/2014 4:11:11 PM)

I agree with K, a PartialRouter could also have worked.
e.g
public class WelcomePartialRouter : IPartialRouter
{
public PartialRouteData GetPartialVirtualPath(
string content, string language, RouteValueDictionary routeValues, RequestContext requestContext)
{
return new PartialRouteData
{
BasePathRoot = requestContext.GetContentLink(),
PartialVirtualPath =
string.Format(CultureInfo.InvariantCulture, "{0}/", content)
};
}

public object RoutePartial(PageData content, SegmentContext segmentContext)
{
if (segmentContext == null)
{
return content;
}

// Optional: check if PageData is of the type you want it to be , else it works on all pages.

SegmentPair nextSegment = segmentContext.GetNextValue(segmentContext.RemainingPath);

string customerKey = nextSegment.Next;

if (string.IsNullOrWhiteSpace(customerKey))
{
return content;
}

segmentContext.RemainingPath = nextSegment.Remaining;

// Do some logic with your customer key.

return content;
}
}

Register the partial route in an InitializableModule
....

public void Initialize(InitializationEngine context)
{
...

WelcomePartialRouter partialRouter = new WelcomePartialRouter();
RouteTable.Routes.RegisterPartialRouter(partialRouter);
...
}
....

Please login to comment.