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.