What's wrong with these asp.net mvc routing rules? - asp.net

I have defined a series of Routes in Global.asax.cs:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "", // Only matches the empty URL (i.e. ~/)
new
{
controller = "Stream",
action = "Entry",
streamUrl = "Pages",
entryUrl = "HomePage"
}
);
routes.MapRoute(null, "{streamUrl}", // matches ~/Pages
new { controller = "Stream", action = "List" }
);
routes.MapRoute(null, "{streamUrl}/{entryUrl}", // matches ~/Pages/HomePage
new { controller = "Stream", action = "Entry" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
When I put in mydomain.com/ or mydomain.com/Pages/HomePage the route works exactly as I expect. So now I'm writing a partial view to generate a list of links. as a test I put this code in the partial view:
<ul>
<% foreach (var item in Model) { %>
<li id="<%:item.Text.Replace( " ", "-") %>">
//This works - link shows up in browser as mydomain.com/
<%: Html.RouteLink(item.Text, new{ streamUrl = "Pages", entryUrl = "HomePage" }) %>
//This does not work - link shows up as mydomain.com/Blog?entryUrl=BlogEntryOne
//instead of mydomain.com/Blog/BlogEntryOne
<%: Html.RouteLink(item.Text, new{ streamUrl = "Blog", entryUrl = "BlogEntryOne" }) %>
</li>
<% } %>
</ul>
I'm not sure why the entryUrl route value isn't being registered correctly. What am I missing?

I'm not terribly accustomed with MVC, but I think you should put your more specific route first, as in:
routes.MapRoute(null, "{streamUrl}/{entryUrl}", // matches ~/Pages/HomePage
new { controller = "Stream", action = "Entry" }
);
routes.MapRoute(null, "{streamUrl}", // matches ~/Pages
new { controller = "Stream", action = "List" }
);

Related

html.ActionLink using wrong route

I have set up the following routes so that I can use duplicate controller names (in different namespaces). This works fine but when I use html.actionlink from any controller it always includes the “CRUD” subfolder to the link.
var route1 = routes.MapRoute(
"CRUD",
"CRUD/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
route1.DataTokens["Namespaces"] = new string[] { "College.Controllers.CRUD" };
route1.DataTokens["UseNamespaceFallback"] = false;
var route2 = routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "College.Controllers" }
);
route2.DataTokens["Namespaces"] = new string[] { "College.Controllers" };
route2.DataTokens["UseNamespaceFallback"] = false;
So an html.actionlink in http://localhost/students/index looks like this
http://localhost/CRUD/students/Edit/1
What I want is this
http://localhost/students/Edit/1
I know I could fix this by specifying the route in the actionlink but I don’t want to do this because I want to re-scaffold in future and my changes would be overwritten.
The issue here is that your 2 routes are ambiguous when building the URL. There are basically 3 ways to fix this:
Add another route value to match that is not part of the URL.
Use RouteLink to specify the route by name (along with the other route value criteria to make it match).
Create a custom route to handle "constraining" it to a specific namespace or make a custom route constraint.
Since you explicitly stated the second option is unacceptable, here is an example of the first:
var route1 = routes.MapRoute(
"CRUD",
"CRUD/{controller}/{action}/{id}",
new { crud = "crud", action = "Index", id = UrlParameter.Optional }
);
route1.DataTokens["Namespaces"] = new string[] { "College.Controllers.CRUD" };
route1.DataTokens["UseNamespaceFallback"] = false;
var route2 = routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
route1.DataTokens["Namespaces"] = new string[] { "College.Controllers" };
route1.DataTokens["UseNamespaceFallback"] = false;
Now when you call #Html.ActionLink("Students", "Index", "Students", new { crud = (string)null }, null) it will not match the CRUD route, it will match the Default route.
To make it match the CRUD route, you have to explicitly add the route value to the ActionLink: #Html.ActionLink("Students", "Index", "Students", new { crud = "crud" }, null) or leave it out entirely: #Html.ActionLink("Students", "Index", "Students")
Constraining the Route
Here is an example of the 3rd option.
Unfortunately, we can't use a regular route constraint because Microsoft decided not to make the RequestContext object available in the IRouteConstraint interface. This means the namespace information about what controller the request is bound for is not available. So, we need to drop to a lower level and make a custom RouteBase class that implements the decorator pattern to wrap our existing Route class configuration.
This class simply checks to see if the namespace from the request matches a specific namespace before generating the URL.
public class NamespaceConstrainedRoute : RouteBase
{
private readonly string namespaceToMatch;
private readonly RouteBase innerRoute;
public NamespaceConstrainedRoute(string namespaceToMatch, RouteBase innerRoute)
{
if (string.IsNullOrEmpty(namespaceToMatch))
throw new ArgumentNullException("namespaceToMatch");
if (innerRoute == null)
throw new ArgumentNullException("innerRoute");
this.namespaceToMatch = namespaceToMatch;
this.innerRoute = innerRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
return innerRoute.GetRouteData(httpContext);
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object namespaces;
if (requestContext.RouteData.DataTokens.TryGetValue("Namespaces", out namespaces)
&& namespaces is IList<string>
&& ((IList<string>)namespaces).Contains(namespaceToMatch))
{
return innerRoute.GetVirtualPath(requestContext, values);
}
// null indicates to try to match the next route in the route table
return null;
}
}
Usage
var route1 = new Route(
url: "CRUD/{controller}/{action}/{id}",
defaults: new RouteValueDictionary(new { action = "Index", id = UrlParameter.Optional }),
routeHandler: new MvcRouteHandler()
)
{
DataTokens = new RouteValueDictionary
{
{ "Namespaces", new string[] { "College.Controllers.CRUD" }},
{ "UseNamespaceFallback", false }
}
};
var route2 = new Route(
url: "{controller}/{action}/{id}",
defaults: new RouteValueDictionary(new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
routeHandler: new MvcRouteHandler()
)
{
DataTokens = new RouteValueDictionary
{
{ "Namespaces", new string[] { "College.Controllers" }},
{ "UseNamespaceFallback", false }
}
};
routes.Add(
name: "CRUD",
item: new NamespaceConstrainedRoute(
namespaceToMatch: "College.Controllers.CRUD",
innerRoute: route1));
routes.Add(
name: "Default",
item: new NamespaceConstrainedRoute(
namespaceToMatch: "College.Controllers",
innerRoute: route2));
From this point, you could build your own MapRoute extension methods if you so choose to make the above configuration look cleaner.

URL rewriting using similar MapRoutes

I have 2 similar maproute requests but I'm trying to target different routes.
Basically I'm creating a picture project using ASP.NET MVC.
What I want is to have the URL as:
website.com/pictures/username
and
website.com/pictures/pictureid
I'm using this as the map routes atm. Hoped that the different signatures would be enough to distinguish which action i would need.
The pictures controller has the action methods as
ActionResult Index (string username) { ... }
ActionResult Index (long id) { ... }
routes.MapRoute(
"UsersPicturesRoute",
"Pictures/{username}",
new { controller = "Pictures", action = "Index", username = UrlParameter.Optional }
);
routes.MapRoute(
"SinglePictureRoute",
"Pictures/{id}",
new { controller = "Pictures", action = "Index", id = UrlParameter.Optional }
);
Is there a way to have this desired outcome?
You can change your RegisterRoutes in below sequence then you will get your required output
routes.MapRoute(
"SinglePictureRoute",
"Pictures/{id}",
new { controller = "Home", action = "abcd", id = UrlParameter.Optional },
new { id = #"\d+" } // Parameter constraints
);
routes.MapRoute(
"UsersPicturesRoute",
"Pictures/{username}",
new { controller = "Home", action = "abcTest", username = UrlParameter.Optional }
);

How to use MapRoute, using ActionLink()?

I have this route values inside Global.asax
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
routes.MapRoute(
"Edit", // Route name
"Admin/{controller}/{action}/{id}", // URL with parameters
new { controller = "Edit", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
and I use this ActionLink method to call the Edit route
#Html.ActionLink("Edit", "Topic", "Edit", new { id = item.ID })
Now the result of the link generated is like this...
http://localhost:777/Admin/Topic?Length=4
How to use the route and target properly using the ActionLink method.
Thanks!
Use the correct overload of ActionLink to get the intended result
#Html.ActionLink("Edit", "Topic", "Edit", new { id = item.ID }, null)
The overload is ActionLink(string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
Adding the null as null HTML attributes is necessary when you supply parameters to the action. Or if you actually needed to apply HTML Attributes to the link, you would use:
#Html.ActionLink("Edit", "Topic", "Edit", new { id = item.ID }, new { #class = "MyCustomCssClassName" } )

MVC3 Areas how to use dashes in page names

We're using Areas with a project. And we're also using this fix to replaces dashes in page names
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new HyphenatedRouteHandler())
);
public class HyphenatedRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
This Fix works fine with the normal top level pages. Home/some-page.
But when it comes to an Area this no longer works. I tried modifying the AreaRegistration.cs file to be an equivalent to the Global.asax.cs route but that didn't work either. I can rename the pages with [ActionName("some-page")] but it doesn't solve the problem of the controller still having underscores Area/some_folder/some-page and I don't want my URLs to look like that.
Edit:
When I use the route that Darin Dimitrov suggest I get this error:
Multiple types were found that match the controller named 'page'. This can happen if the route that services this request ('admin/{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
We have multiple "segments" that have the same names but different content to target the segment audience
The following should work in your area registration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(
"Admin_default",
new Route("Admin/{controller}/{action}/{id}",
new RouteValueDictionary(
new { action = "Index", id = UrlParameter.Optional }
),
null,
new RouteValueDictionary(
new
{
area = AreaName
}
),
new HyphenatedRouteHandler()
)
);
}
UPDATE:
You seem to be having the same controller name in multiple areas which is not possible without defining a namespace constraint when registering your routes as the error message you are getting suggests you to do.
So in your Global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(
"Default",
new Route(
"{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
),
null,
new RouteValueDictionary(
new { Namespaces = new[] { "AppName.Controllers" } }
),
new HyphenatedRouteHandler()
)
);
}
and in your AreaRegistration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(
"Admin_default",
new Route(
"Admin/{controller}/{action}/{id}",
new RouteValueDictionary(
new { action = "Index", id = UrlParameter.Optional }
),
null,
new RouteValueDictionary(
new
{
Namespaces = new[] { "AppName.Areas.Admin.Controllers" },
area = AreaName
}
),
new HyphenatedRouteHandler()
)
);
}
You might need to adjust the namespace in the constraint to match yours.

Map a route to the same controller action

I want only one controller action to handle all GETs. How can I map a route to do this?
routes.MapRoute("AllGETs",
"{*any}",
new { Controller = "YourController", Action = "YourAction" },
new { HttpMethod = new HttpMethodConstraint("GET") }
);
I actually ended up doing this, seemed to do what I needed:
routes.MapRoute(
// Route name
"Default",
// URL with parameters
"{controller}/{id}",
// Parameter defaults
new {controller = "Home", action = "GenericPostHandler", id = "" }
);

Resources