MVC5 Rewriting Routing Attribute - Default page - asp.net-mvc-5

I am attempting to switch from RouteConfig to Routing Attributes.
I am following along the Pro ASP.NET MVC 5 book from Adam Freeman and I'm trying to convert the following code that handles the paging of clients.
routes.MapRoute(
name: null,
url: "{controller}/Page{page}",
defaults: new { action = "Index", status = (string)null },
constraints: new { page = #"\d+" }
);
This works great! As I go to different URLs, the links look very nice
http://localhost:65534/Client - Default page
http://localhost:65534/Client/Page2 - Second page
Now I've decided to try out Url Attributes and having a bit of problems when it comes to how 'pretty' the links are. All of the links are working fine, but it's the 'routing rewriting' that I am trying to fix.
Here are the important parts of my controller.
[RoutePrefix("Client")]
[Route("{action=index}/{id:int?}")]
public class ClientController : Controller {
[Route("Page{page:int?}")]
public ActionResult Index(string sortOrder, string search = null, int page = 1) {
With the attribute above the Index, going to /Client or to /Client/Page gives me a 404.
Adding a blank route to catch the default page
[Route("Page{page:int?}")]
[Route]
Works for /Client and /Client/Page3, but now the rewriting of the URL is messed up. Clicking on page 3 of the pager gives me a URL of
http://localhost:65534/Client?page=3
which is not what I want. Changing the routing to
[Route("Page{page:int?}")]
[Route("{page=1:int?}")]
Works almost 100%, but the default link for /Client is now
http://localhost:65534/Client/Page
So, I am now asking for help. How can I correctly convert the original MapRoute to the attributes?

Just use:
[Route("", Order = 1)]
[Route("Page{page:int}", Order = 2)]
UPDATE
Plainly and simply, the routing framework is dumb. It doesn't make decisions about which route is the most appropriate, it merely finds a matching route and returns. If you do something like:
Url.Action("Index", "Client", new { page = 1 })
You're expecting the generated URL to be /Client/Page1, but since you have a route where page is essentially optional, it always will choose that route and append anything it can't stuff into the URL as a querystring, i.e. /Client?page=1. The only way to get around this is to actually name the route you want and use that named route to generate the URL. For example:
[Route("", Order = 1)]
[Route("Page{page:int}", Name = "ClientWithPage", Order = 2)]
And then:
Url.RouteUrl("ClientWithPage", new { page = 1 })
Then, you'll get the route you expect because you're directly referencing it.
UPDATE #2
I'm not sure what you mean by "go into PagedList.MVC and add a name property to it". It doesn't require any core changes to the code because PagedList already has support for custom page links. Just change your pager code to something like:
#Html.PagedListPager((IPagedList)ViewBag.OnePageOfItems, page => Url.RouteUrl("ClientWithPage", new { page = page }))
And you'll get the URL style you want. Attribute routing can be a bit more finicky than traditional routing, but I'd hardly call it useless. It's far more flexible than traditional routing, but that flexibility has some costs.

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>

Orchard CMS front-end all possible content filtering by user permissions

Good day!
In my Orchard, I have several content types all with my custom part attached. This part defines to what users this content is available. For each logged user there is external service, which defines what content user can or cannot access. Now I need access restriction to apply everywhere where orchard display content lists, this includes results by specific tag from a tag cloud, or results listed from Taxonomy term. I seems can’t find any good way to do it except modifying TaxonomyServices code as well as TagCloud services, to join also my part and filter by it. Is this indeed the only way to do it or there are other solutions? I would like to avoid doing changes to built-in modules if possible but cannot find other way.
Thanks in advance.
I'm currently bumbling around with the same issue. One way I'm currently looking at is to hook into the content manager.
[OrchardSuppressDependency("Orchard.ContentManagement.DefaultContentManager")]
public class ModContentManager : DefaultContentManager, IContentManager
{
//private readonly Lazy<IShapeFactory> _shapeFactory;
private readonly IModAuthContext _modAuthContext;
public ModContentManager(IComponentContext context,
IRepository<ContentTypeRecord> contentTypeRepository,
IRepository<ContentItemRecord> contentItemRepository,
IRepository<ContentItemVersionRecord> contentItemVersionRepository,
IContentDefinitionManager contentDefinitionManager,
ICacheManager cacheManager,
Func<IContentManagerSession> contentManagerSession,
Lazy<IContentDisplay> contentDisplay,
Lazy<ISessionLocator> sessionLocator,
Lazy<IEnumerable<IContentHandler>> handlers,
Lazy<IEnumerable<IIdentityResolverSelector>> identityResolverSelectors,
Lazy<IEnumerable<ISqlStatementProvider>> sqlStatementProviders,
ShellSettings shellSettings,
ISignals signals,
//Lazy<IShapeFactory> shapeFactory,
IModAuthContext modAuthContext)
: base(context,
contentTypeRepository,
contentItemRepository,
contentItemVersionRepository,
contentDefinitionManager,
cacheManager,
contentManagerSession,
contentDisplay,
sessionLocator,
handlers,
identityResolverSelectors,
sqlStatementProviders,
shellSettings,
signals) {
//_shapeFactory = shapeFactory;
_modAuthContext = modAuthContext;
}
public new dynamic BuildDisplay(IContent content, string displayType = "", string groupId = "") {
// So you could do something like...
// var myPart = content.As<MyAuthoPart>();
// if(!myPart.IsUserAuthorized)...
// then display something else or display nothing (I think returning null works for this but
//don't quote me on that. Can always return a random empty shape)
// else return base.BuildDisplay(content, displayType, groupId);
// ever want to display a shape based on the name...
//dynamic shapes = _shapeFactory.Value;
}
}
}
Could also hook into the IAuthorizationServiceEventHandler, which is activated before in the main ItemController and do a check to see if you are rendering a projection or taxonomy list set a value to tell your content manager to perform checks else just let them through. Might help :)

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>

MVC 5 AttributeRouting Catch All

How do I create a catch all route with the new Attribute routing in MVC
I tried this:
[Route("{pagenode}", Order = 999)]
But when I have a named route like
[Route("contact"]
I get the "Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL." error.
This can be done with Attribute Routing if the first "directory" in the path is fixed.
For example, to match anything that hits /questions or /questions/4 or /questions/answers/42 then you would use [Route("questions/{*catchall}"].
You can't do this with Attribute routing, do this the MVC4 way:
Map a route in your routemapper like this:
routes.MapRoute("RouteName","{*url}",new { controller = "YourFancyController", action = "YourAction" });
This will be your catch-all Route.
If you would like to map all the routes to their controller you can do this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
AreaRegistration.RegisterAllAreas();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
The ability to do this must have changed.
In my default controller, still called 'Home' I have one result method which I want executed for an unrecognised URL structure. The routing attribute is: [Route("{*catchall}")]. This is successfully executed for any old thing.
I have a second method which is always successfully executed based on its route (and I've thrown a few route 'styles' at it to see if it always works). I can only assume that the framework always registers the catch-all route last as this is the behaviour I'm seeing.
This is also a brand new, not configured (except for nuGet packages) MVC 5 project excepting that my methods have been changed to return JsonResult (not even doing their job yet but returning little anonymously typed objects). The catch-all for example returns: Json(new { Message = "Invalid Request" }, JsonRequestBehavior.AllowGet). Yes, yes I set the StatusCode first etc etc, this isn't about MY project ;).
I'm sure I haven't left anything out since there's so little to it but if any clarification is wanted I'll see about adding it.

How to set new Orchard module to be Home Page via code

I'm very new with orchard.
To learn orchard module development, I followed the documentation and tried to create a commerce module.
The module consists of product part and product type which has product part.
During enable module, it will create admin and home menu for this module, "Commerce" and "Shop" respectively.
My questions are
How do I make this module to be home page during enable module. In other word, I want Index method of
the module's HomeController handle home url?
How do I get Shop menu in front end to be after home menu or register this module to home menu?
I am attaching source code, please download it from the following link
download source code
To take over the home page the standard Orchard way is to implement IHomePageProvider.
You can, when creating a page as part of migrations.cs in a module, tell the Autoroute part to set your created page's alias as the homepage:
//create a page page
var homepage = _contentManager.Create("Page");
homepage.As<TitlePart>().Title = "My Home";
_contentManager.Publish(homepage);
var homePageArp = homepage.As<AutoroutePart>();
homePageArp.DisplayAlias = String.Empty;
_autorouteService.PublishAlias(homePageArp);
This assumes you're going from a clean instance of Orchard without any prior homepages; if you have an existing homepage, you'll have to regenerate those pages' Aliases as part of your module too. This is how it's done as part of the AutoroutePartHandler in the Orchard.Autoroute project (inside the Publish Alias method):
// regenerate the alias for the previous home page
var currentHomePages = _orchardServices.ContentManager.Query<AutoroutePart, AutoroutePartRecord>().Where(x => x.DisplayAlias == "").List();
foreach (var current in currentHomePages) {
if (current != null) {
current.CustomPattern = String.Empty; // force the regeneration
current.DisplayAlias = _autorouteService.Value.GenerateAlias(current);
}
_autorouteService.Value.PublishAlias(current);
}
_autorouteService.Value.PublishAlias(part);
If you dig through the driver and handler for the autoroute project, you'll learn a lot about the internals; when you tick that "set as homepage" box in the Admin UI, it sets the Path to "/" and then that gets picked up, triggers the old homepage re-wire, clears the "/" path to String.Empty and then publishes that blank alias, giving you a new homepage.
(this is valid as of Orchard 1.6)
If your module is to be used by others, then it is better to make a widget which can be added to any layer (the homepage layer for example). That way each user can decide where your module comes into play.
If you are using this module for yourself only, then you can just override the default routes (standard mvc functionallity).
Look at my ExtendedRegistration module (Routes.cs) to see how it's done.
Here I am overriding the standard Account/Register URL. There should be nothing preventing you from overriding the default HomeController.
public class Routes : IRouteProvider
{
public void GetRoutes(ICollection<RouteDescriptor> routes)
{
foreach (var routeDescriptor in GetRoutes())
{
routes.Add(routeDescriptor);
}
}
public IEnumerable<RouteDescriptor> GetRoutes()
{
return new[] {
new RouteDescriptor {
Priority = 19,
Route = new Route(
"Users/Account/Register",
new RouteValueDictionary {
{"area", "itWORKS.ExtendedRegistration"},
{"controller", "Account"},
{"action", "Register"}
},
new RouteValueDictionary(),
new RouteValueDictionary {
{"area", "itWORKS.ExtendedRegistration"}
},
new MvcRouteHandler())
}
};
}
}

Resources