November Happy Hour will be moved to Thursday December 5th.

Routing Problem

Vote:
 

Hi,

I have an Mvc controller for a page type with mutiple actions and additional parameters to the action methods. E.g.

public ActionResult List(MyPage currentPage, int page = 1, string sortColumn = "ColA", string sortOrder = "asc")
{
   ....
}


I have registered a custom content route in an initialization module as follows:

RouteTable.Routes.MapContentRoute(
           name: "ListPage",
           url: "{language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}",
           defaults: new { controller = "ListPage", action = "Index", page = UrlParameter.Optional, sortColumn = UrlParameter.Optional, sortOrder = UrlParameter.Optional });

If I browse to the page with just the {lang}, {node} and {actions} segments in the Url : /en/thepage/list then it works. If I try and add any of the segments, such as page : /en/thepage/list/1 then I get a 404. 

Any ideas?

Thanks

#121971
May 22, 2015 13:41
Vote:
 

Where route is registered in the RouteTable? I mean what is the index for that route?

#121994
May 24, 2015 22:23
Vote:
 

Do you get 404 when you browse /en/thepage/list/index/1?

If you get 200, then you can take look into this blogpost: http://www.mogul.com/en/about-mogul/blog/registering-a-route-with-an-optional-action-segment-in-episerver-75-and-on

BR,

Marija

#122035
May 26, 2015 9:31
Vote:
 

My bad, I haven't read through it, you are not calling the index action.

#122036
May 26, 2015 9:32
Vote:
 

I used Alloy website, and added Created ProductPageController:

public class ProductPageController : PageControllerBase<ProductPage>
{
    public ActionResult Index(ProductPage currentPage)
    {
        var viewModel = PageViewModel.Create(currentPage);
        return View(viewModel);
    }

    public string List(ProductPage currentPage, int page = 1, string sortColumn = "ColA", string sortOrder = "asc")
    {
        return string.Format("page: {0} | sortColumn: {1} | sortOrder: {2}", page, sortColumn, sortOrder);
    }
}

Here's the code that handles routing:

public static class SegmentRouteCollectionExtensions
{
    public static ContentRoute MapContentRouteWithSegments(this RouteCollection routes, string name, string url,
                                                            object defaults, params ISegment[] segments)
    {
        return routes.MapContentRouteWithSegments(name, url, defaults, null, segments);
    }

    public static ContentRoute MapContentRouteWithSegments(this RouteCollection routes, string name, string url,
                                                            object defaults, MapContentRouteParameters parameters,
                                                            params ISegment[] segments)
    {
        if (parameters == null)
        {
            parameters = new MapContentRouteParameters();
        }

        if (parameters.SegmentMappings == null)
        {
            parameters.SegmentMappings = new Dictionary<string, ISegment>();
        }

        foreach (var segment in segments)
        {
            parameters.SegmentMappings.Add(segment.Name, segment);
        }

        return routes.MapContentRoute(name, url, defaults, parameters);
    }
}

public class CustomSegment : SegmentBase
{
    public CustomSegment(string name) : base(name)
    {
    }

    public override bool RouteDataMatch(SegmentContext context)
    {
        string path = context.RemainingPath;
        // set the default value if there is no remaining path
        if (string.IsNullOrEmpty(path))
        {
            if (!context.Defaults.ContainsKey(Name))
            {
                return false;
            }

            context.RouteData.Values[Name] = context.Defaults[Name];
            return true;
        }

        var segments = path.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
        context.RemainingPath = GetRemainingPath(segments);
        context.RouteData.Values[Name] = segments[0];

        return true;
    }

    public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
    {
        if (!values.ContainsKey(Name))
            return null;

        object value = values[Name];
        var valueType = value.GetType();

        // if value is an enumerable or array, concatenate the list to a string
        if (valueType.IsGenericType && value is IEnumerable)
        {
            IEnumerable<string> valueList = from object val in value as IEnumerable
                                            select Convert.ToString(val);

            value = string.Join("/", valueList);
        }
        else if (valueType.IsArray)
        {
            var valueArray = value as object[];

            if (valueArray != null)
            {
                value = string.Join("/", valueArray);
            }
        }

        return string.Format("{0}/", value);
    }

    private string GetRemainingPath(string[] path)
    {
        if (path.Length == 0 || path.Length == 1)
        {
            return string.Empty;
        }

        return string.Join("/", path, 1, path.Length - 1);
    }
}

[InitializableModule]
[ModuleDependency(typeof(ServiceContainerInitialization))]
public class CustomRoutesModule : IInitializableModule
{

    public void RegisterRoutes(RouteCollection routes)
    {
        routes.MapContentRouteWithSegments("ProductPageListing",
                                            "{language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}",
                                            new
                                            {
                                                controller = "ProductPage",
                                                action = "Index",
                                                page = UrlParameter.Optional,
                                                sortColumn = UrlParameter.Optional,
                                                sortOrder = UrlParameter.Optional
                                            },
                                            new CustomSegment("page"),
                                            new CustomSegment("sortColumn"),
                                            new CustomSegment("sortOrder")
            );
    }

    public void Initialize(InitializationEngine context)
    {
        RegisterRoutes(RouteTable.Routes);

    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    public void Preload(string[] parameters)
    {
    }
}

http://localhost:54428/alloy-plan/list returns: page: 1 | sortColumn: ColA | sortOrder: asc
http://localhost:54428/alloy-plan/list/2 returns: page: 2 | sortColumn: ColA | sortOrder: asc
http://localhost:54428/alloy-plan/list/2/a returns: page: 2 | sortColumn: a | sortOrder: asc
http://localhost:54428/alloy-plan/list/2/a/b returns: page: 2 | sortColumn: a | sortOrder: b

Hope this helps :)

#122063
May 26, 2015 12:07
Vote:
 

Thanks Dejan. I'll try this out.

Paul.

#122064
May 26, 2015 12:13
Vote:
 

Hi again Dejan. Unfortunately that solution doesn't work for me. The CustomSegment.RouteDataMatch is called for every segment after the {lang} segment. I.e, the method gets called for "alloy-plan" which it adds to RouteData with key "page". I have other {node} segments which then continue to incorrectly be matched against the optional parameters.

#122066
May 26, 2015 12:37
Vote:
 

How about this simplified version with contraints:

public class ProductPageListContraint : IContentRouteConstraint
{
    private readonly IContentLoader _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();

    public bool Match(Route route, SegmentContext segmentContext, string parameterName)
    {

        if (ContentReference.IsNullOrEmpty(segmentContext.RoutedContentLink))
        {
            return false;
        }

        var content = _contentLoader.Get<IContent>(segmentContext.RoutedContentLink);

        // constraint 1: Content must be of type Product Page
        if (content is ProductPage)
        {
            // constraint 2: action must be list
            if (segmentContext.RouteData.Values.ContainsKey("action") &&
                string.Equals(segmentContext.RouteData.Values["action"] as string, "list",
                                StringComparison.CurrentCultureIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }
}

[InitializableModule]
[ModuleDependency(typeof(ServiceContainerInitialization))]
public class CustomRoutesModule : IInitializableModule
{
    public void RegisterRoutes(RouteCollection routes)
    {
        RouteTable.Routes.MapContentRoute(null, "{language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}",
                                            new
                                            {
                                                controller = "ProductPage",
                                                action = "Index",
                                                page = UrlParameter.Optional,
                                                sortColumn = UrlParameter.Optional,
                                                sortOrder = UrlParameter.Optional
                                            },
                                            new MapContentRouteParameters
                                            {
                                                Constraints = new { x = new ProductPageListContraint()}
                                            });

            
    }

    public void Initialize(InitializationEngine context)
    {
        RegisterRoutes(RouteTable.Routes);
    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    public void Preload(string[] parameters)
    {
    }
}

Also, the order of routes in RegisterRoutes method matters.

For example, {language}/{node}/{action}/{bla} should be registered before {language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}

#122085
May 26, 2015 14:35
Vote:
 

No, that doesn't work either. The constraint's match method is only ever called for the SysRoot page :(

#122086
May 26, 2015 15:05
Vote:
 

There's clearly something funny in my solution as I've tried your Alloy example and it works perfectly. I'll keep looking.

#122087
May 26, 2015 15:20
Vote:
 

The problem seems to be that the CustomSegment.RouteDataMatch method seems to be called for the 2nd and subsequent parts of the {node} segment. So given the url http://mysite/en/page1/page2/page3/list/3/colA/asc with a route of {language}/{node}/{action}/{page}/{sortColumn}/{sortOrder} registered I would expect :

en to map to {lang}

page1/page2/page3 to map to {node}

list to map to {action}

3 to map to {page}

colA to map to {sortColumn}

and asc to map to {sortOrder}

However, what happens is that the CustomSegment.RouteDataMatch method is first called with 'page2'. I would expect CustomSegment.RouteDataMatch to be first called with 3 (i.e. the first custom segment)

I don't have any other custom routes registered in the whole codebase.

#122096
May 26, 2015 16:46
Vote:
 

My first code is broken :(

The second one should work. If ProductPageListContraint.Match method is triggered for SysRoot page only, that meens that some other route is triggered first.

Can you use some router debugger tool to check which one? Atm, I'm having problems with Alloy on EPi 8.6 and Glimpse for MVC 4 :(

Can we reproduce your custom-route scenario on Alloy?

#122098
May 26, 2015 16:55
Vote:
 

I got your first code sample working on Alloy. Neither work in my solution. Will look into route debugging.

#122104
May 26, 2015 17:09
Vote:
 

Something totally weird that I don't understand, when CustomSegment.RouteDataMatch is called, the RoutedContent is always for content ref 1 (i.e. recycle bin).

#122112
May 26, 2015 18:16
Vote:
 

Hi

There is no need to create custom segments or constraints to pass urlsegments as parameters. This is handled by default by the route registration. In my case i tested with alloy MVC package I registered a route as:

 routes.MapContentRoute(
               name: "ListPage",
               url: "{language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}",
               defaults: new { 
                   controller = "ListPage", 
                   action = "Index", 
                   page = UrlParameter.Optional, 
                   sortColumn = UrlParameter.Optional, 
                   sortOrder = UrlParameter.Optional 
               });

What will happen here during registration is that each segment after action (that is page, sortColumn and sortOrder) will be handled in the route by an instance of EPiServer.Web.Routing.Segments.ParameterSegment. This segment is pretty trivial, it will just take the value of the segment from the url (if present) and put it in RouteData.Values. Later during ModelBinding there is a modelbinder that binds values from RouteData.Values to the parameters for action (where the name of the parameter matches the key in RouteData.Values).

So in my case I registered an action on PageControllerBase as:

        public ActionResult List(SitePageData currentPage, int page = 1, string sortColumn = "ColA", string sortOrder = "asc")
        {
            return Content(String.Format("Called for currentPage:'{0}' with parameters page='{1}', sortColumn='{2}' ans sortOrder='{3}'",
                currentPage.Name, page, sortColumn, sortOrder));
        }

and it works as expected.

My guess is that you are runnning on some 7.x version? If so then in 7.x is custom routes by default registered with ContentReference.RootPage as start for the route. So to match your route should the url be /en/Start/thepage/list/1. We have changed this in EPiServer 8.x so custom routes by default is registered with ContentReference.StartPage as root, then your route should work as expected. In EPiServer 7.x you can register your route with the root set to startpage for your route as: 

 

  routes.MapContentRoute(
               name: "ListPage",
               url: "{language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}",
               defaults: new { 
                   controller = "ListPage", 
                   action = "Index", 
                   page = UrlParameter.Optional, 
                   sortColumn = UrlParameter.Optional, 
                   sortOrder = UrlParameter.Optional 
               }, 
               contentRootResolver: (siteDefinition) => siteDefinition.StartPage);

 

#122137
May 27, 2015 9:56
Vote:
 

It seems that controller="ListPage" can be removed as well? At least in EPi 8.6

#122149
May 27, 2015 10:16
Vote:
 

edit:

I believe constraints are needed when there's more than one custom route.

Here's an example from Alloy on EPi 8.6

I have 2 controllers:

public class ProductPageController : PageControllerBase<ProductPage>
{
    public string List(ProductPage currentPage, int page = 1, string sortColumn = "ColA", string sortOrder = "asc")
    {
        return string.Format("product page - page: {0} | sortColumn: {1} | sortOrder: {2}", page, sortColumn, sortOrder);
    }
}

public class StandardPageController : PageControllerBase<StandardPage>
{
    public string List(StandardPage currentPage, int page1 = 1, string sortColumn1 = "ColA", string sortOrder1 = "asc")
    {
        return string.Format("standard page - page1: {0} | sortColumn1: {1} | sortOrder1: {2}", page1, sortColumn1,
                                sortOrder1);
    }
}

And the following routes:

RouteTable.Routes.MapContentRoute(null, "{language}/{node}/{action}/{page}/{sortColumn}/{sortOrder}",
new
{
	controller = "ProductPage",
	action = "Index",
	page = UrlParameter.Optional,
	sortColumn = UrlParameter.Optional,
	sortOrder = UrlParameter.Optional
});

RouteTable.Routes.MapContentRoute(null, "{language}/{node}/{action}/{page1}/{sortColumn1}/{sortOrder1}",
new
{
	controller = "StandardPage",
	action = "Index",
	page1 = UrlParameter.Optional,
	sortColumn1 = UrlParameter.Optional,
	sortOrder1 = UrlParameter.Optional
});

When I run http://localhost:54428/about-us/list/2/3/4 without route constraints, I get:

standard page - page1: 1 | sortColumn1: ColA | sortOrder1: asc

And if I add route constraints, I get:

standard page - page1: 2 | sortColumn1: 3 | sortOrder1: 4

#122151
May 27, 2015 10:40
Vote:
 

Thanks Johan. works perfectly!

#122190
May 27, 2015 18:27
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.