#Html.PagedListPager does not send page in query string - pagination

I am working on ASP.NET Core project and after I updated the project to .NET Core 2.2 an issue with pagination has аppeared. I am using X.PagedList library.
In my view I have the correct number of pages but the problem is that when i try to access the second page for example, the page is not sent in the query string.
This is my code in the view:
#Html.PagedListPager((IPagedList)Model.Products,
page => Url.Action("ProductsByCategory",
new { Model.CategoryId, page, Model.SubCategoryId }),
new PagedListRenderOptions()
{
UlElementClasses = new List<string> { "pagination"},
LiElementClasses = new List<string> { "page-item", "page-link"}
})
This is my code in the service:
public AllProductsViewModel GetProductsByCategory(Guid categoryId, int? page, Guid? subCategoryId = null)
{
var products = dbContext.Products
.Where(p => p.CategoryId == categoryId && p.IsAvailable)
.To<ProductViewModel>()
.ToList();
if (subCategoryId != null)
{
products = products.Where(p => p.SubCategoryId == subCategoryId).ToList();
}
var nextPage = page ?? 1;
var allProducts = new AllProductsViewModel()
{
CategoryId = categoryId,
SubCategoryId = subCategoryId,
Products = products.ToPagedList(nextPage, 9)
};
return allProducts;
}

That seems a bug using X.PagedList in .Net Core 2.2 , please check the related threads on github :
https://github.com/dncuug/X.PagedList/issues/133
https://github.com/dncuug/X.PagedList/issues/131
So try to use pageNumber instead of page as a workaround .

The issue is down to a change in the way asp.net core 2.2 constructs its URLs. By default it now checks there's a matching endpoint in the application, and if there isn't, no href is outputted.
Routing differences between asp.net core 2.2 and earlier versions
I had pages already indexed by search engines that used the querystring ?page and I didn't want to have to include 301s to redirect ?page to ?pagenumber.
Instead, there's an option in startup where you can override this behaviour and allow X.PagedList to work still using page rather than pagenumber.
Where you add MVC disable the endpoint routing feature:
opt.EnableEndpointRouting=false
services.AddMvc(opt => opt.EnableEndpointRouting=false)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Related

How to return a different Razor Page without redirect?

I am porting a Asp.Net MVC application to Razor Pages.
In some of the controllers of the MVC application it makes use of return View("someOtherView", someModelForOtherView);
How do I port this to Razor Pages?
What I need to do is to transfer the request over to another Razor Page and pass the prepared PageModel to it (the other page does not need to execute OnMethod() but simply render its html.
Or, in other words, I only need to swap the template file that should be rendered with another one.
I cannot use Redirect as there must not be another roundtrip via the browser.
I doubt this is (easily) possible. From the github request that Lerner linked above, it's noted Razor Pages weren't designed to do that.
The closest workaround I was able to achieve was to turn my destination Razor Page into a View. (Hence, no code-behind.) Obviously that will only be possible if your destination page is never directly accessed via URL. For example, if you want to redirect to /Pages/MyPage, and you still need to be able to access the url http://example.com/MyPage, this won't work.
But, say all you want is a generic error or status page. Those don't have to be directly-accessible through URL. This works well for that.
Here's a couple extension methods on PageModel to do it, one that accepts models and one that doesn't:
public static ViewResult View(this PageModel pageModel, string viewName) {
return new ViewResult() {
ViewName = viewName,
ViewData = pageModel.ViewData,
TempData = pageModel.TempData
};
}
public static ViewResult View<TModel>(this PageModel pageModel, string viewName, TModel model) {
var viewDataDictionary = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary()) {
Model = model
};
foreach (var kvp in pageModel.ViewData) viewDataDictionary.Add(kvp);
return new ViewResult {
ViewName = viewName,
ViewData = viewDataDictionary,
TempData = pageModel.TempData
};
}
FYI, the reason for having to recreate the view dictionary is because the one in your pageModel is going to have a model type specific to the current Page, not to the View you're directing to, and you can't change the Model within a ViewDataDictionary to a different type. MVC would complain and throw an exception.
Usage:
public IActionResult OnGet(string id) {
// check if id is good here
if (idIsNoGood) return this.View("InvalidId", new ErrorModel...);
else {
return Page();
}
}
The above will look for InvalidId.cshtml view, which can be in the same folder as your page, the root /Pages/ folder, or /Pages/Shared/. And it'll still use your Layout too, like any other page.
Just make sure your cshtml file doesn't have a #page directive at the top; this won't work for a Razor page, only a View.
Example InvalidId.cshtml:
#model MyProject.Models.ErrorModel
<h1>Invalid Request</h1>
<p>#Model.Message</p>

Change culture for an ASP.NET Core 2 Razor page

I created an ASP.NET Core 2 projects with razor pages and I would like to give the opportunity to the visitor to select a language. The first problem that I had was to change the web application url so that ti will include the current language code. I solved this problem by adding the following code in ConfigureServices.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
options.Conventions.AddFolderRouteModelConvention("/", model =>
{
foreach (var selector in model.Selectors)
{
var attributeRouteModel = selector.AttributeRouteModel;
attributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{language=el-GR}", attributeRouteModel.Template);
}
});
});
}
}
Now I could visit a page using the following URL:
http://domain/el-GR/MyPage
The last thing that I would like to do is to change the culture of each request. The best solution that I fount which I do not like is to put the following code in my page:
System.Globalization.CultureInfo.CurrentCulture = new System.Globalization.CultureInfo((string)RouteData.Values["language"]);
System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo((string)RouteData.Values["language"]);
This is not nice because I will have to add these lies in every razor page that I will create in my project.
Is there another way to set the culture for all the requests of my web application?
Refer to this article: https://joonasw.net/view/aspnet-core-localization-deep-dive
There are a few methods, I use the RequestCultureProviders.
NuGet: Microsoft.AspNetCore.Localization
in my Startup.Configure method.
IList<CultureInfo> sc = new List<CultureInfo>();
sc.Add(new CultureInfo("en-US"));
sc.Add(new CultureInfo("zh-TW"));
var lo = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = sc,
SupportedUICultures = sc
};
var cp = lo.RequestCultureProviders.OfType<CookieRequestCultureProvider>().First();
cp.CookieName = "UserCulture"; // Or whatever name that you like
app.UseRequestLocalization(lo);
Set your cookie "UserCulture" to "c=zh-TW|uic=zh-TW" once.
And it works magically.

Set PageSize in a Taxonomy view

I've started my first project using OrchardCMS and so far so good. It's a brilliant piece of kit and amazingly powerful once you get up to speed with the terminology of it.
One area I'm struggling with is displaying Taxonomies. There seems to be almost no documentation on it, and Shape Tracing is not being very useful.
I'm using a taxonomy to display content items that have the same term. This is all working great now, however the default pagination page size is 10 items. I've dug through lots of code and found this is driven from TermPart
protected override DriverResult Display(TermPart part, string displayType, dynamic shapeHelper) {
return Combined(
ContentShape("Parts_TermPart_Feed", () => {
// generates a link to the RSS feed for this term
_feedManager.Register(part.Name, "rss", new RouteValueDictionary { { "term", part.Id } });
return null;
}),
ContentShape("Parts_TermPart", () => {
var pagerParameters = new PagerParameters();
var httpContext = _httpContextAccessor.Current();
if (httpContext != null) {
pagerParameters.Page = Convert.ToInt32(httpContext.Request.QueryString["page"]);
}
var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
var taxonomy = _taxonomyService.GetTaxonomy(part.TaxonomyId);
var totalItemCount = _taxonomyService.GetContentItemsCount(part);
var partSettings = part.Settings.GetModel<TermPartSettings>();
if (partSettings != null && partSettings.OverrideDefaultPagination) {
pager.PageSize = partSettings.PageSize;
}
The question is where is TermPart & TermPartSettings defined?
Thanks in advance
The TermPartSettings are in the Modules/Orchard.Taxonomies/Settings folder. The TermPart itself is in the Modules/Orchard.Taxonomies/Models directory. Don't know what you want with that information though, because you shouldnt edit an Orchard's module code.
When you create a new Taxonomy in the dashboard of Orchard, a new content type is created under Content Definition. It is called YourTaxonomy Term. This content type has a setting called 'override the default page size', where you can define your own page size.

Defalut XmlSiteMapProvider implementation cannot use SiteMap.FindSiteMapNode?

I just upgrade MvcSiteMapProvider from v3 to v4.6.3.
I see the upgrade note indicate:
In general, any reference to System.Web.SiteMap.Provider will need to be updated to MvcSiteMapProvider.SiteMaps.Current
I am trying to get the sitemap node by using:
SiteMaps.Current.FindSiteMapNode(rawUrl)
But it always return null
I looked into the code. In the sitemap it's actually calling the function:
protected virtual ISiteMapNode FindSiteMapNodeFromUrlMatch(IUrlKey urlToMatch)
{
if (this.urlTable.ContainsKey(urlToMatch))
{
return this.urlTable[urlToMatch];
}
return null;
}
It's trying to find a match in the urlTable.
I am using Default implementation of XmlSiteMapProvider .
It define var url = node.GetAttributeValue("url");
siteMapNode.Url = url;
siteMapNode.UrlResolver = node.GetAttributeValue("urlResolver");
So if I did not define url or urlResolver attribute in the .sitemap file. These variables a set to empty string, when generate the node.
And when this nodes are passed to AddNode function in SiteMap.
When adding the node
bool isMvcUrl = string.IsNullOrEmpty(node.UnresolvedUrl) && this.UsesDefaultUrlResolver(node);
this code will check if there is url or urlResolver
// Only store URLs if they are clickable and are configured using the Url
// property or provided by a custom URL resolver.
if (!isMvcUrl && node.Clickable)
{
url = this.siteMapChildStateFactory.CreateUrlKey(node);
// Check for duplicates (including matching or empty host names).
if (this.urlTable
.Where(k => string.Equals(k.Key.RootRelativeUrl, url.RootRelativeUrl, StringComparison.OrdinalIgnoreCase))
.Where(k => string.IsNullOrEmpty(k.Key.HostName) || string.IsNullOrEmpty(url.HostName) || string.Equals(k.Key.HostName, url.HostName, StringComparison.OrdinalIgnoreCase))
.Count() > 0)
{
var absoluteUrl = this.urlPath.ResolveUrl(node.UnresolvedUrl, string.IsNullOrEmpty(node.Protocol) ? Uri.UriSchemeHttp : node.Protocol, node.HostName);
throw new InvalidOperationException(string.Format(Resources.Messages.MultipleNodesWithIdenticalUrl, absoluteUrl));
}
}
// Add the URL
if (url != null)
{
this.urlTable[url] = node;
}
Finally no url is add to the urlTable, which result in FindSiteMapNode cannot find anything.
I am not sure if there needs to be specific configuration. Or should I implement custom XmlSiteMapProvider just add the url.
ISiteMapNodeProvider instances cannot use the FindSiteMapNode function for 2 reasons. The first you have already discovered is that finding by URL can only be done if you set the url attribute explicitly in the node configuration. The second reason is that the SiteMapBuilder doesn't add any of the nodes to the SiteMap until all of the ISiteMapNodeProvider instances have completed running, so it would be moot to add the URL to the URL table anyway.
It might help if you explain what you are trying to accomplish.
The ISiteMapNodeProvider classes have complete control over the data that is added to the SiteMapNode instances and they also have access to their parent SiteMapNode instance. This is generally all that is needed in order to populate the data. Looking up another SiteMapNode from the SiteMap object while populating the data is not supported. But as long as the node you are interested in is populated in the same ISiteMapNodeProvider instance, you can just get a reference to it later by storing it in a variable.
Update
Okay, I reread your question and your comment and it now just seems like you are looking in the wrong place. MvcSiteMapProvider v4 is no longer based on Microsoft's SiteMap provider model, so using XmlSiteMapProvider doesn't make sense, as it would sidestep the entire implementation. The only case where this might make sense is if you have a hybrid ASP.NET and ASP.NET MVC application that you want to keep a consitant menu structure between. See Upgrading from v3 to v4.
There are 2 stages to working with the data. The first stage (the ISiteMapBuilder and ISiteMapNodeProvider) loads the data from various sources (XML, .NET attributes, DynamicNodeProviders, and custom implementations of ISiteMapNodeProvider) and adds it to an object graph that starts at the SiteMap object. Much like Microsoft's model, this data is stored in a shared cache and only loaded when the cache expires. This is the stage you have been focusing on and it definitely doesn't make sense to lookup nodes here.
The second stage is when an individual request is made to access the data. This is where looking up data based on a URL might make sense, but there is already a built-in CurrentNode property that finds the node matching the current URL (or more likely the current route since we are dealing with MVC) which in most cases is the best approach to finding a node. Each node has a ParentNode and ChildNodes properties that can be used to walk up or down the tree from there.
In this second stage, you can access the SiteMap data at any point after the Application_Start event such as within a controller action, in one of the built in HTML helpers, an HTML helper template in the /Views/Shared/DisplayTemplates/ directory, or a custom HTML helper. This is the point in the application life cycle which you might call the lines SiteMaps.Current.FindSiteMapNode(rawUrl) or (more likely) SiteMaps.Current.CurrentNode to get an instance of the node so you can inspect its Attributes property (the custom attributes).
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
var currentNode = MvcSiteMapProvider.SiteMaps.Current.CurrentNode;
string permission = currentNode.Attributes.ContainsKey("permission") ? currentNode.Attributes["permission"].ToString() : string.Empty;
string programs = currentNode.Attributes.ContainsKey("programs") ? currentNode.Attributes["programs"].ToString() : string.Empty;
string agencies = currentNode.Attributes.ContainsKey("agencies") ? currentNode.Attributes["agencies"].ToString() : string.Empty;
// Do something with the custom attributes of the About page here
return View();
}
The most common usage of custom attributes is to use them from within a custom HTML helper template. Here is a custom version of the /Views/Shared/DisplayTemplates/SiteMapNodeModel.cshtml template that displays the custom attributes. Note that this template is called recursively by the Menu, SiteMapPath, and SiteMap HTML helpers. Have a look at this answer for more help if HTML helper customization is what you intend to do.
#model MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel
#using System.Web.Mvc.Html
#using MvcSiteMapProvider.Web.Html.Models
#if (Model.IsCurrentNode && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper") {
<text>#Model.Title</text>
} else if (Model.IsClickable) {
if (string.IsNullOrEmpty(Model.Description))
{
#Model.Title
}
else
{
#Model.Title
}
} else {
<text>#Model.Title</text>
}
#string permission = Model.Attributes.ContainsKey("permission") ? Model.Attributes["permission"].ToString() : string.Empty
#string programs = Model.Attributes.ContainsKey("programs") ? Model.Attributes["programs"].ToString() : string.Empty
#string agencies = Model.Attributes.ContainsKey("agencies") ? Model.Attributes["agencies"].ToString() : string.Empty
<div>#permission</div>
<div>#programs</div>
<div>#agencies</div>

ServiceStack - Redirecting at root based on query params [duplicate]

This question already has answers here:
Create route for root path, '/', with ServiceStack
(3 answers)
Closed 3 years ago.
I've got a Fallback DTO that looks like the following:
[FallbackRoute("/{Path*}")]
public class Fallback
{
public string Path { get; set; }
}
Now, in my Service I would like to redirect to an HTML5 compliant URL, and this is what I've tried:
public object Get(Fallback fallback)
{
return this.Redirect("/#!/" + fallback.Path);
}
It is working all fine and dandy, except for the fact that query parameters are not passed along with the path. Using Request.QueryString does not work as no matter what I do it is empty. Here's what my current (non-working) solution looks like:
public object Get(Fallback fallback)
{
StringBuilder sb = new StringBuilder("?");
foreach (KeyValuePair<string, string> item in Request.QueryString)
{
sb.Append(item.Key).Append("=").Append(item.Value).Append("&");
}
var s = "/#!/" + fallback.Path + sb.ToString();
return this.Redirect(s);
}
TL;DR: I want to pass on query strings along with fallback path.
EDIT: It turns out I had two problems; now going to mysite.com/url/that/does/not/exist?random=param correctly redirects the request to mysite.com/#!/url/that/does/not/exist?random=param& after I changed the above loop to:
foreach (string key in Request.QueryString)
{
sb.Append(key).Append("=").Append(Request.QueryString[key]).Append("&");
}
But the fallback is still not being called at root, meaning mysite.com/?random=param won't trigger anything.
In essence, what I want to do is to have ServiceStack look for query strings at root, e.g., mysite.com/?key=value, apply some logic and then fire off a redirect. The purpose of this is in order for crawler bots to be able to query the site with a _escaped_fragment_ parameter and then be presented with an HTML snapshot prepared by a server. This is in order for the bots to be able to index single-page applications (more on this).
I'm thinking perhaps the FallbackRoute function won't cover this and I need to resort to overriding the CatchAllHandler.
I managed to find a solution thanks to this answer.
First create an EndpointHostConfig object in your AppHost:
var config = new EndpointHostConfig
{
...
};
Then, add a RawHttpHandler:
config.RawHttpHandlers.Add(r =>
{
var crawl = r.QueryString["_escaped_fragment_"];
if (crawl != null)
{
HttpContext.Current.RewritePath("/location_of_snapshots/" + crawl);
}
return null;
});
Going to mysite.com/?_escaped_fragment_=home?key=value will fire off a redirection to mysite.com/location_of_snapshots/home?key=value, which should satisfy the AJAX crawling bots.
N.B. It's possible some logic needs to be applied to the redirection to ensure that there won't be double forward slashes. I have yet to test that.

Resources