Join us this Friday for AI in Action at the Virtual Happy Hour! This free virtual event is open to all—enroll now on Academy and don’t miss out.

 

PartialRouter for Pagination

Vote:
 

I have a page type of BlogListPage with a controller:

public class BlogListPageController : PageController<BlogListPage>
{
    public ActionResult Index(BlogListPage currentPage, int? page = 1, BlogFiltersRequest filters = null)
    {
        var model = new BlogListPageViewModel(currentPage);
          
        return View(PopulateViewModel(model, page ?? 1, filters));
    }
}

I am able to call this page: /blog and /blog?page=3, but I want to be able to call page 3 using this url: /blog/3

I have implemented a Partial Route and registed it

public class BlogPaginationPartialRouting : IPartialRouter<BlogListPage, BlogListPage>
{
    public object RoutePartial(BlogListPage content, UrlResolverContext urlResolverContext)
    {
        // get the next url segment, which should be the post url
        var continentPart = urlResolverContext.GetNextRemainingSegment(urlResolverContext.RemainingPath);
         // check if part is an int
        if (continentPart != null && int.TryParse(continentPart.Next, out int page))
        {
            // add page parameter
            urlResolverContext.RouteValues.Add("page", (int?)page);
            // set the rest of the path
            urlResolverContext.RemainingPath = continentPart.Remaining;
        }
        // return the blog or null
        return content;
    }

    public PartialRouteData GetPartialVirtualPath(BlogListPage content, UrlGeneratorContext urlGeneratorContext)
    {
        return new PartialRouteData
        {
            BasePathRoot = content.ContentLink,
            PartialVirtualPath = string.Empty
         };
    }
}
_services.AddSingleton<IPartialRouter, BlogPaginationPartialRouting>();

When I run and set breakpoints and call /blog/3, BlogPaginationPartialRouting.RoutePartial() is hit and the page value is set. And the the controller Index action is hit, but the page parameter is set to the default of 1 and not to 3.

How can I set the action parameter for the page value in the IPartialRouter?

I found this https://world.optimizely.com/forum/developer-forum/CMS/Thread-Container/2017/3/custom-routing-with-extra-parameters-in-action-method post, but it is for older version.

#276128
Mar 11, 2022 16:20
Vote:
 

I think in CMS 12 you have a simpler option available to you. In the configure method of your startup class you should have a call to endpoints.MapContent(). There's an extension method you can call on that called "MapTemplate" which allows you to register a custom route pattern for your controller which you could call like this:

app.UseEndpoints(endpoints =>
{
    endpoints.MapContent()
        .MapTemplate<BlogListPageController>("{page}");
});

You then shouldn't need the partial router at all.

#276331
Mar 14, 2022 17:08
Paul Gruffydd - Mar 14, 2022 17:22
Actually, thinking about it, you might be better changing that MapTemplate call to:
MapTemplate
Vote:
 

Paul,

Prior to adding this issue, I tried and I got an error (see below)

app.UseEndpoints(endpoints =>
{
  endpoints.MapContent()
  .MapTemplate<BlogListPageController>("{action}/{page:int}");  
});

then based on your reply, I just tried

app.UseEndpoints(endpoints =>
{
  endpoints.MapContent()
    .MapTemplate<BlogListPageController>("{page:int}");
  endpoints.MapControllers();
  endpoints.MapRazorPages();
}

and

app.UseEndpoints(endpoints =>
{
  endpoints.MapContent()
    .MapTemplate<BlogListPageController>("{page}");
  endpoints.MapControllers();
  endpoints.MapRazorPages();
}

But I'm getting this error when going to /blog/3 which is the same error I got when I included the action in the pattern

ArgumentNullException: Value cannot be null. (Parameter 'propertyName')
EPiServer.Framework.Validator.ThrowIfNullOrEmpty(string name, string value)
EPiServer.Web.Internal.BlobResolver.ResolveProperty(IContentData content, string propertyName)
EPiServer.Core.Routing.Internal.BlobPartialRouter.RoutePartial(IContent content, UrlResolverContext segmentContext)
EPiServer.Core.Routing.PartialRouter<TIncoming, TOutgoing>.RoutePartial(IContent content, UrlResolverContext urlResolverContext)
EPiServer.Core.Routing.Pipeline.Internal.PartialUrlResolverPipelineStep.Resolve(UrlResolverContext context, UrlResolverOptions options)
EPiServer.Core.Routing.Internal.DefaultContentUrlResolver.Resolve(Uri url, UrlResolverOptions options)
EPiServer.Web.Routing.Internal.PipelineUrlResolver.Route(UrlBuilder urlBuilder, RouteArguments routeArguments)
EPiServer.Web.Routing.Matching.Internal.ContentMatcherPolicy.MapEndpointAsync(Endpoint originalEndpoint, HttpContext httpContext)
EPiServer.Web.Routing.Matching.Internal.ContentMatcherPolicy.ApplyAsync(HttpContext httpContext, CandidateSet candidates)
Microsoft.AspNetCore.Routing.Matching.DfaMatcher.SelectEndpointWithPoliciesAsync(HttpContext httpContext, IEndpointSelectorPolicy[] policies, CandidateSet candidateSet)
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatch|8_1(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
#276333
Mar 14, 2022 17:23
Vote:
 

Looking at that error, I wonder whether there's a route already registered which is being a bit over zealous in what it's handling. It looks like the request is being processed by SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddleware which has presumably spotted the integer at the end of the URL and assumed it's an image dimension.

#276334
Mar 14, 2022 17:33
Vote:
 

If it is that, maybe try changing the route pattern to something like this:

MapTemplate<BlogListPageController>("p{page:int}/")

then, rather than /blog/3 it would be /blog/p3

#276335
Mar 14, 2022 17:38
Josh Salwen - Apr 21, 2022 21:37
I got it working with the help of the support team. It issue is that "page" is a reserved word. Your solution also worked when I changed to:
"{pageNum:int}
Johan Kronberg - Nov 14, 2022 17:09
Hi Josh! Did you need to do anything else besides changing to `urlResolverContext.RouteValues.Add("pageNum", (int?)page);` and `public ActionResult Index(BlogListPage currentPage, int? pageNum = 1`?
Josh Salwen - Nov 14, 2022 20:20
Johan, see my lasted entry on this. Using the MapTemplate works. We did not need to use the IPartialRouter.
Vote:
 

I disabled the SixeLabors code and I still get an error. I tried tried:

endpoints.MapContent()
  .MapTemplate<BlogListPageController>("{action}/p{page:int}");

endpoints.MapContent()
  .MapTemplate<BlogListPageController>("{action}/{page:int}");

endpoints.MapContent()
  .MapTemplate<BlogListPageController>("{page:int}");

Error: 

ArgumentNullException: Value cannot be null. (Parameter 'propertyName')
EPiServer.Framework.Validator.ThrowIfNullOrEmpty(string name, string value)
EPiServer.Web.Internal.BlobResolver.ResolveProperty(IContentData content, string propertyName)
EPiServer.Core.Routing.Internal.BlobPartialRouter.RoutePartial(IContent content, UrlResolverContext segmentContext)
EPiServer.Core.Routing.PartialRouter<TIncoming, TOutgoing>.RoutePartial(IContent content, UrlResolverContext urlResolverContext)
EPiServer.Core.Routing.Pipeline.Internal.PartialUrlResolverPipelineStep.Resolve(UrlResolverContext context, UrlResolverOptions options)
EPiServer.Core.Routing.Internal.DefaultContentUrlResolver.Resolve(Uri url, UrlResolverOptions options)
EPiServer.Web.Routing.Internal.PipelineUrlResolver.Route(UrlBuilder urlBuilder, RouteArguments routeArguments)
EPiServer.Web.Routing.Matching.Internal.ContentMatcherPolicy.MapEndpointAsync(Endpoint originalEndpoint, HttpContext httpContext)
EPiServer.Web.Routing.Matching.Internal.ContentMatcherPolicy.ApplyAsync(HttpContext httpContext, CandidateSet candidates)
Microsoft.AspNetCore.Routing.Matching.DfaMatcher.SelectEndpointWithPoliciesAsync(HttpContext httpContext, IEndpointSelectorPolicy[] policies, CandidateSet candidateSet)
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatch|8_1(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

NOTE: One of the reason I was going down the path of using IPartialRouter was so the Canonical URL will be correct and include the page number (not yet implemented in code). 

#276336
Mar 14, 2022 18:41
Vote:
 

I added a note, but to close this out, the issue was that page is a reserved word, so we used pageNum.

In Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapContent()
            // pagination routing
            .MapTemplate<BlogListPageController>("{pageNum:int}");
        });
}

In BlogListPageController.cs:

public ActionResult Index(BlogListPage currentPage, int? pageNum = 1)
{
   var model = new BlogListPageViewModel(currentPage);
            
    return View(PopulateViewModel(model, pageNum ?? 1));
}
#291653
Nov 14, 2022 20:18
Johan Kronberg - Nov 15, 2022 6:43
Ah, great!

I couldn't get urlResolverContext.RouteValues.Add() to work with my Partial Router so instead I just kept the default look of the Index action method and tucked away the URL part I needed from the the partial router into HttpContext instead.

Then just did a short lookup for it early in the Controller.
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.