mvcsitemapprovider - Navigating to the same ActionResult from different pages - c#

Mvc 5, .Net 4.5
I implement MvcSiteMapNodeAttribute as follows:
[MvcSiteMapNode(Title="Running Events", ParentKey="Events", Key="RunningEvents")]
public ActionResult RunningEvents()
{
return View();
}
I need to access this page from multiple locations and keep the breadcrumbs in tack (i.e. from the correct calling method). However the ParentKey dictates where the call comes from and thus set the ParentNode based on it. This is not ideal as I want the calling ActionResult to be the parent and not "hard coded" as with the ParentKey solution. The ParentKey is also not editable at runtime nor the ParentNode. The only way around this at the moment is to duplicate the ActionResult with different signatures and give it the same Title which is also not ideal.
I've read up on mvc routing, DynamicNodeProvider, route mapping, etc but cannot find a way to make this work? I'm also not very familiar with mvc so would appreciate some guidance.
Thanks

See the Multiple Navigation Paths to a Single Page documentation.
You can use the same controller action in multiple places. However, you must always provide a unique set of route values (which usually means each URL should be unique).
The most natural way to do this is to design your URLs with the parent category.
routes.MapRoute(
name: "Category1RunningEvents",
url: "Category1/RunningEvents",
defaults: new { controller = "Events", action = "RunningEvents", category="Category1" }
);
routes.MapRoute(
name: "Category2RunningEvents",
url: "Category2/RunningEvents",
defaults: new { controller = "Events", action = "RunningEvents", category="Category2" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
And then differentiate the route matching by the category field. You can use multiple MvcSiteMapNode attributes on the same action, each with different parent key in the SiteMap.
[MvcSiteMapNode(Title="Category 1 Events", ParentKey="Events", Key="RunningEvents", Attributes = #"{ ""category"": ""Category1"" }")]
[MvcSiteMapNode(Title="Category 2 Events", ParentKey="Category2", Key="Category2RunningEvents", Attributes = #"{ ""category"": ""Category2"" }")]
public ActionResult RunningEvents()
{
return View();
}
Of course, this isn't the only way to configure the routing but it should clear up the concept. The only limitation is that you must use a unique set of route values for the match, each which corresponds to a node. However, there can be multiple nodes that represent the same controller action, each with a different parent node.
Also see this answer.

Related

Get third parameter in asp.net MVC

I have a asp.net MVC website, and I have this url:
Home/Index/
There is one action that I want to do, and currently I'm doing it using query string, like this:
Home/Index/?action=1
I'll then get it in my code using Request.QueryString["action"].
But this does not look as good as it should, I'd like to have something like this:
Home/Index/Action
So the query string would be a third parameter. How can I use it like this and check if the third parameter exists, and it's name?
My routes are configured like this:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Do I need to change anything on the routes or I can just enter the way I want, and if so, how do I retrieve this value?
Edit :
Alright. What I wanted to do was to have a different result in the view depending on this parameter.
What I did now was create two ActionResults that use the same View sending different values to the ViewData.
In MVC there is the concept of Model Binding, where the names of the parameters are used and then filled with values from the Form, without you having to do it.
If you write your Action method like this:
public ActionResult Index(int action)
{ ... }
then it will already work for /Home/Index?action=1.
If you want to be able to call it like /Home/Index/1 then the easiest way is to simply write the Action method like this:
public ActionResult Index(int id)
{ ... }
This works because the MapRoute definition basically says 'if there is something after the {action} part (which in this case is "Index", by the way), then give it the parameter name {id} and bind it to the {id} parameter of my Action method".
Update:
The problem with using a parameter called action in both calling scenarios is that {action} is a special keyword in MVC.
To use MySpecialParam for both calling scenarios, you could just add this Route:
routes.MapRoute(
name: "Default_MySpecialParam",
url: "{controller}/{action}/{MySpecialParam}",
defaults: new { controller = "Home", action = "Index", MySpecialParam = UrlParameter.Optional }
);
However if MySpecialParam were to become action, then there is a name resolution conflict that MVC probably won't know how to handle, or it may even throw an error.
The action is supposed to be right after the controller, usually. I think in your case it might be better to add a new controller, which has the action you want.
I gave up mapping routes as this is cumbersome. You need to add a new route for every number of parameters, like follows:
routes.MapRoute(
"ThreeIds", // Route name
"{controller}/{action}/{firstId}/{secondId}/{thirdId}" // URL with parameters
);
routes.MapRoute(
"Dates", // Route name
"{controller}/{action}/{startDate}/{endDate}" // URL with parameters
);
Try doing ajax calls using something like jQuery instead. No nead to add a route for every possibility.
$.ajax({
url: '../' + controller + '/' + method,
data: jsonObjectString,
contentType: 'application/json',
dataType: 'json',
success: function (data) {
//do something
},
error: function (err) {
//do something
}
});
You can use my library at https://github.com/Biot-Savart/MVC-DataCall.js to make life a bit easier.

If Route doesn't exist -> go to other route using its default action?

So I'm having a little problem here with routing.
There are two parts to this web application:
  1. Brochure / Display Website
  2. Internal Site / Client Application
We wanted a way to release changes for the brochure without having to do a whole release of said Web application.
Visiting existing named views will take the user to a brochure page, however if it doesn't exist, it will act like they are a client and will redirect them to their company's login screen.
Global.asax:
//if view doesnt exist then url is a client and should be redirected
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
);
//This is home page
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
Controller:
/// <summary> Check if the view exists in our brochure list </summary>
private bool ViewExists(string name) {
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
return (result.View != null);
}
/// <summary> Generic action result routing for all pages.
/// If the view doesn't exist in the brochure area, then redirect to interal web
/// This way, even when we add new pages to the brochure, there is no need to re-compile & release the whole Web project. </summary>
public ActionResult Brochure(string id) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToRoute("HomeDefault", new { client = id });
}
This code works fine up until we log in and go to the landing page. It seems to keep the Brochure action in the route and doesn't want to go to the subsequent controller which results in a 500 error.
e.g. 'domain/client/Brochure' when it needs to be: 'domain/client/Index'
Things tried but not worked:
Changing RedirectToRoute() to a RedirectToAction() - this results in a
finite loop of going back to the ActionResult Brochure(). So
changing controllers through that didn't work.
Create an ActionResult called Brochure() inside the 'HomeController'. It
doesn't even get hit.
Passed in namespaces for RedirectToRoute() as an attribute. I knew this would probably not work, but it was worth a try.
So the question is:
How can I get the route to act properly?
If you can restrict id to some subset of all values you can add that constraints to route (i.e. numbers only) to let default handle the rest.
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
constraints : new { category = #"\d+"}
);
If you can't statically determine restrictions - automatically redirecting in your BrochureController similar to your current code would work. The only problem with sample in the question is it hits the same route again and goes into infinite redirect loop - redirect to Url that does not match first rule:
// may need to remove defaults from second route
return RedirectToRoute("HomeDefault", new { client = id, action = "index" });
If standard constraints do not work and you must keep single segment in url - use custom constraints - implement IRouteConstraint and use it in first route. See Creating custom constraints.
There are several issues with your configuration. I can explain what is wrong with it, but I am not sure I can set you on the right track because you didn't provide the all of the URLs (at least not all of them from what I can tell).
Issues
Your Brouchure route, which has 1 optional URL segment named {id}, will match any URL that is 0 or 1 segments (such as / and /client). The fact that it matches your home page (and you have another route that is named HomeDefault that will never be given the chance to match the home page) leads me to believe this wasn't intended. You can make the {id} value required by removing the default value id = "Index".
The Brouchure route has a namespace that indicates it is probably in an Area. To properly register the area, you have to make the last line of that route ).DataTokens["area"] = "Brochure"; or alternatively put it into the /Areas/Brouchure/AreaRegistration.cs file, which will do that for you.
The only way to get to the HomeDefault route is to supply a 2 segment URL (such as /client/Index, which will take you to the Index method on the HomeController). The example URLs you have provided have 3 segments. Neither of the routes you have provided will match a URL with 3 segments, so if these URLs are not getting 404 errors they are obviously matching a route that you haven't provided in your question. In other words, you are looking for the problem in the wrong place.
If you provide your entire route configuration including all Area routes and AttributeRouting routes (including the line that registers them), as well as a complete description of what URL should go to what action method, I am sure you will get more helpful answers.
So the question is:
How can I get the route to act properly?
Unknown. Until you describe what properly is.
Related: Why map special routes first before common routes in asp.net mvc?
Two ways I could have solved this issue:
Way 1
I reviewed the redirect and just passed in an action in order to get a route that has 2 segments in the url. i.e. client/Index. The Index action now handles logins - going past a custom controller.
public class HomeController : CustomController
public ActionResult Brochure(string id, string action) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToAction("Index", "Home", new { client = id, action = "Index" });
}
Way 2
(from #Alexei_Levenkov)
Create a custom Route constraint so the route will be ignored if the view cannot be found.
namespace Web.Contraints {
public class BrochureConstraint : IRouteConstraint {
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//Create our 'fake' controllerContext as we cannot access ControllerContext here
HttpContextWrapper context = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "brochure");
ControllerContext controllerContext = new ControllerContext(new RequestContext(context, routeData), new BrochureController());
//Check if our view exists in the folder -> if so the route is valid - return true
ViewEngineResult result = ViewEngines.Engines.FindView(controllerContext, "~/Areas/Brochure/Views/Brochure/" + values["id"] + ".cshtml", null);
return result.View != null;
}
}
}
namespace Web {
public class MvcApplication : System.Web.HttpApplication {
//If view doesnt exist then url is a client so use the 'HomeDefault' route below
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" },
constraints: new { isBrochure = new BrochureConstraint() }
);
//This is home page for client
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
}
}
I hope this helps someone else out there.

MVC Route Mapping

I have a website, xyz.com and users are setting up admin accounts where they then have other users that are registering under them. They call their account name wisconsinsponsor. Another user sets up another account called iowasponsor. So I want to be able to have the ability that a user could browse to xyz.com/wisconsinsponsor and xyz.com/iowasponsor and get funneled into the appropriate settings that these users have setup.
So then after I browse to xyz.com/wisconsinsponsor which will allow me to get the appropriate settings for wisconsinsponsor I can be dropped onto xyz.com/wisconsinsponsor/{controller}/{method}.
So I added the following code.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
List<Sponsor> sponsors = new SponsorContext().Sponsors.ToList();
foreach (Sponsor sponsor in sponsors)
{
// ALL THE PROPERTIES:
// rentalProperties/
routes.MapRoute(
name: sponsor.SponsorName,
url: sponsor.SponsorName + "{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = sponsor.SponsorId
}
);
}
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional }
);
}
So the main goal is that without logging in, I can get information that pertains to each "sponsor" and then just generic information if a user goes to 'xyz.com' without specifying a sponsor. The below works to a point for landing on the home page, but then when I navigate to login or any other view, I get for example 'xyz.com/[my first sponsor entry in the database]/admin/login' instead of 'xyz.com/admin/login'. Why doesn't the navigation fall to the Default route?
Change your route to simply include the sponsor, don't create individual routes for every sponsor, this is where routing becomes so powerful
routes.MapRoute(
name: null, //routes names must be unique, but you can have multiple named null (go figure)
url: "{sponsor}/{controller}/{action}/{id}",
defaults: new
{
sponsor = "defaultSponsor", //or whatever you want the default to be
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
Then, decorate each of your action methods with the sponsor argument
public ActionResult Index(string sponsor, int id) { }
Rereading your question tho, this doesn't help in the instance of not having a sponsor, unless your "defaultSponsor" is not really a sponsor, but your generic information that is presented. So when no sponsor is passed in the address bar to the routing, you see 'defaultSponsor' or and empty string and could then handle appropriately
Another reason to handle it this way, is RegisterRoutes is only called upon application start up, so if they were dynamically added while the app is running, they would be invalid routes until the application is restarted. By making it an argument, they would work dynamically as well.

Understanding ASP.NET MVC Routes reg

I've created a system in MVC using the NerdDinner tutorial as a base to work off.
Everything was working fine until I used single action methods such as
Here is the global.asax.cs
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "mysample", id=UrlParameter.Optional }
);
which routes to
http:localhost/Home/mysample
i just want to create routes which has more than one action in the sense
http:localhost/<controller>/<action>/<params>
ex: localhost/mycontroller/myaction/details/myname
Any help much appreciated.
Thanks.
update 1:
i have writen router like this as
routes.MapRoute(
"myname", // Route name
"{controller}/{action}/{details}/{myname}", // URL with parameters
new { controller = "mycontroller", action = "myaction", details= "details", myname= "" } // Parameter defaults
);
and retried the value with following syntax as
String name=RouteData.Values["myname"].ToString();
it works fine .
but even though the url called as
localhost/mycontroller/myaction/details
its being routed to that controller and error is being thrown as null reference...
how to avoid it?
You can't define multiple actions in one MVC route.
In MVC routing configuration is used for mapping your Controlers and Actions to user friendly routes and:
Keep URLs clean
Keep URLs discoverable by end-users
Avoid Database IDs in URL
Understanding default route config:
routes.MapRoute(
name: "Default", // Route name
routeTemplate: "{controller}/{action}/{id}", // URL with parameters
defaults: new { controller = "Home", action = "mysample", id=UrlParameter.Optional }
);
The "routeTemplate" property on the Route class defines the Url
matching rule that should be used to evaluate if a route rule applies
to a particular incoming request.
The "defaults" property on the Route class defines a dictionary of
default values to use in the event that the incoming URL doesn't
include one of the parameter values specified.
Default route will map all requests, because it has defined default values for every property in routeTemplate, {} means that property is variable, if you not provide value for that param in URL, it will try to take default value if you provide it. In default route it has defined defaults for controller, action and id param is optional. That means if you have route like this:
.../Account/Login
It will take you to Account controller, Login action and because you didn't specified prop and it is defined as optional it will work.
.../Home
This will also work, and it will take you to Home contoller and mysample action
When you define custom route, like you did:
routes.MapRoute(
"myname", // Route name
"{controller}/{action}/{details}/{myname}", // URL with parameters
new { controller = "mycontroller", action = "myaction", details= "details", myname= "" } // Parameter defaults
);
You didn't specified myname as optional and you didn't specified it your route, that means that your URL: localhost/mycontroller/myaction/details wan't be handled by your custom route myname. It will be handled by default route. And when you try to access your myname param in controller it wan't be there and you will get null reference error. If you want to specifie default value of your parameter if not present in url you need to do that in your controller. For example:
public class MyController : Controller
{
public ActionResult MyAction(string details = "details", string myname = "")
{
...
and change your custom route to:
routes.MapRoute(
"myname", // Route name
"{controller}/{action}/{details}/{myname}", // URL with parameters
new { controller = "mycontroller", action = "myaction", details= UrlParameter.Optional, myname= UrlParameter.Optional } // Parameter defaults
);
But you can define only one controller and only one action, rest of the routeTemplate are parameters.
You can't define two action in one route. It make no sense.

Can't bind to parameter

I've got the default routing:
routes.MapRoute(
"Shortie", // Route name
"{controller}/{id}", // URL with parameters
new { controller = "Ettan", action = "Index", id = "id" } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Ettan", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
I've got a controller: NewsController. It has one method, like this:
public ActionResult Index(int id)
{
...
}
If I browse to /News/Index/123, it works. /News/123 works. However, /News/Index?id=123 does not (it can't find any method named "index" where id is allowed to be null). So I seem to be lacking some understanding on how the routing and modelbinder works together.
The reason for asking is that I want to have a dropdown with different news sources, with parameter "id". So I can select one news source (for instance "sport", id = 123) and it should be routed to my index method. But I can't seem to get that to work.
The ASP.NET MVC Routing works using reflection. It will look inside the controller for a method matching the pattern you are defining in your routes. If it can't find one...well you've seen what happens.
So the answer is (as posted in the comments) to change the type of your id parameter to a Nullable<int> i.e. int?.

Categories