Register custom route in EPiServer 7

PLACE FOR BLOG

Register custom route in EPiServer 7

Register custom route in EPiServer 7

marija

Register route

So, the first time we started with EPi MVC, we immediately had a need to register a custom route to have /news/2 instead of /news?pageid=2 or /news/Index/2. Mmmm, but how?

Define optional parameter segment

First of all, we need to create an optional parameter segment - in order to get rid of the Index in the URL and have it set as default action:

    public class OptionalActionSegment : ParameterSegment
    {
        private readonly IContentLoader _contentLoader;

        public OptionalActionSegment(string name, IContentLoader contentLoader) : base(name)
        {
            _contentLoader = contentLoader;
        }

        public override bool RouteDataMatch(SegmentContext context)
        {
            var segmentPair = context.GetNextValue(context.RemainingPath);
            if (!string.IsNullOrEmpty(segmentPair.Next))
            {
                if (ShouldAddDefaultAction(context))
                {
                    //do not consume segment just add default action
                    context.RouteData.Values[Name] = context.Defaults[Name];
                }
                else
                {
                    context.RouteData.Values[Name] = segmentPair.Next;
                    context.RemainingPath = segmentPair.Remaining;
                }

                return true;
            }
            
            if (context.Defaults.ContainsKey(Name))
            {
                context.RouteData.Values[Name] = context.Defaults[Name];
                return true;
            }

            return false;
        }

        private bool ShouldAddDefaultAction(SegmentContext context)
        {
            var content = _contentLoader.Get(context.RoutedContentLink);
            // perhaps add more page types in future, for search, etc
            return content is IListing;
        }
    }

Register route with optional parameter

Now, we need to register a route with the default action/optional parameter. This is done by overriding EPi's RegisterRoutes in Global : EPiServer.Global.


        protected override void RegisterRoutes(RouteCollection routes)
        {
            base.RegisterRoutes(routes);
            
            IContentLoader contentLoader;
            ServiceLocator.Current.TryGetExistingInstance(out contentLoader);

            IRoutingSegmentLoader routingSegmentLoader;
            ServiceLocator.Current.TryGetExistingInstance(out routingSegmentLoader);
            AncestorReferencesLoader ancestorReferencesLoader;
            ServiceLocator.Current.TryGetExistingInstance(out ancestorReferencesLoader);
            
            var segment = new OptionalActionSegment("action", contentLoader);
            var routingParameters = new MapContentRouteParameters()
            {
                SegmentMappings = new Dictionary(),
                UrlSegmentRouter = new DefaultUrlSegmentRouter(contentLoader,
                new LanguageSelectorFactory(), routingSegmentLoader, ancestorReferencesLoader)
            };
            
            routingParameters.SegmentMappings.Add("action", segment);

            routes.MapContentRoute(
                name: "optionalaction",
                url: "{language}/{node}/{partial}/{action}/{page}",
                defaults: new { action = "index" },
                parameters: routingParameters);
        }

The problem

The above RegisterRoutes method, however, kept issuing an error on very first application start. It worked for me, as I already had the ServiceLocator configured and initialized. But, when someone new downloaded the project from SVN, this was the procedure:

  1. Comment out everything from var segment = new ... and on
  2. Run
  3. Comment in

Wonderful workaround! (*&$#%(&

The basic problem is that at a time RegisterRoutes is executed, EPiServer's internal StructureMap isn't initialized. contentLoader and routingSegmentLoader were always null. RegisterRoutes is always executed before Application_Start, but even when I moved ServiceLocator.Current.Buildup(this); to RegisterRoutes, problem persisted.

The solution

I kept thinking that I can use an IInitializableModule to configure structuremap, but since it executes after RegisterRoutes, that was not the solution. After some re-factoring of Global.asax code to initializable modules, the problem was still there haunting me. Then, I realized that EPiServer has a specific execution cycle related to routing:

  1. RegisterRoutes is called
  2. InitializableModules are initialized
  3. Application_Start is called
  4. Start page is loaded
  5. When I access a listing page, RegisterRoutes is called again!! Woo hoo!! (RegisterRoutes is called again for non-default routes -> when a URL is accessed, EPi checks the route table and if it's empty, they call RegisterRoutes again, to my pure joy).

So, I just checked IF the loaders are null and skipped route registration if they are. My love goes to IF statements (big grin). Final Global.asax became this:

    public class Global : EPiServer.Global
    {
        protected void Application_Start(Object sender, EventArgs e)
        {
            AreaRegistration.RegisterAllAreas();
            AutoMapperConfiguration.Configure();
            ServiceLocator.Current.Buildup(this);            
        }

        protected override void RegisterRoutes(RouteCollection routes)
        {
            base.RegisterRoutes(routes);

            IContentLoader contentLoader;
            ServiceLocator.Current.TryGetExistingInstance(out contentLoader);

            IRoutingSegmentLoader routingSegmentLoader;
            ServiceLocator.Current.TryGetExistingInstance(out routingSegmentLoader);

            if (contentLoader != null && routingSegmentLoader != null)
            {
                var segment = new OptionalActionSegment("action", contentLoader);
                var routingParameters = new MapContentRouteParameters()
                {
                    SegmentMappings = new Dictionary(),
                    UrlSegmentRouter = new DefaultUrlSegmentRouter(
                        contentLoader,
                        new LanguageSelectorFactory(),
                        ContentReference.StartPage,
                        routingSegmentLoader)
                };
                routingParameters.SegmentMappings.Add("action", segment);

                routes.MapContentRoute(
                    name: "optionalaction",
                    url: "{language}/{node}/{partial}/{action}/{page}",
                    defaults: new { action = "index" },
                    parameters: routingParameters);
            }
        }

        protected void Application_End(object sender, EventArgs e)
        {
        }
    }

Conclusion

New lesson learnt, not a quick one, but felt that much better after it was finally fixed!

LEAVE A COMMENT