ASP.NET MVC routing parameters not working with areas - asp.net-mvc

I'm currently playing around with areas and routing within them. What I'm trying to achieve is to be able to have a URL that looks like this;
PracticeAdmin/Practice/[Practice Name]
which I would then be able to add things like Edit and Delete to the end of.
I have achieved this in the past when not working with areas by adding this annotation to the action
[Route("PracticeAdmin/Practices/{practiceName}")]
public ActionResult Details(string practiceName)
this would produce the URLs that I would like. The problem I am having is that when I am trying to do this when using areas I get links that look like this;
PracticeAdmin/Practices?practiceName=Practice1
which is not what I am looking for.
The code that I am using to try and produce this with is
PracticeAdminAreaRegistration.cs
using System.Web.Mvc;
namespace TrainingPortal.Areas.PracticeAdmin
{
public class PracticeAdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "PracticeAdmin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"PracticeAdmin_default",
"PracticeAdmin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new[] { "TrainingPortal.Areas.PracticeAdmin.Controllers" }
);
}
}
}
RouteConfig.cs
namespace TrainingPortal
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "TrainingPortal.Controllers" }
);
}
}
}
I have called MapMvcAttributeRoutes here which I believe should mean that the routes are registered even within areas. I have also tried putting the necessary code within PracticeAdminAreaRegistration to do the same thing with no effect.
PracticeAdminController.cs
namespace TrainingPortal.Areas.PracticeAdmin.Controllers
{
public partial class PracticesController : Controller
{
private TpContext db = new TpContext();
// GET: PracticeAdmin/Practices
public virtual ActionResult Index()
{
return View(db.Practices.ToList());
}
[Route("PracticeAdmin/Practice/{practiceName}")]
public virtual ActionResult Details(string practiceName)
{
if (string.IsNullOrWhiteSpace(practiceName))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Practice practice = db.Practices.FirstOrDefault(m => m.PracticeName.ToLower() == practiceName);
if (practice == null)
{
return HttpNotFound();
}
return View(practice);
}
...
Obviously it carries on with other methods but they all follow the same approach as this one.
Index.cshtml snippet
#Html.ActionLink("Delete", MVC.PracticeAdmin.Practices.Delete(item.PracticeName))
#Html.ActionLink("Delete2", "Delete", new { practiceName = item.PracticeName })
Within PracticeAdminArea/Views/Practices/Index.cshtml I have tried using both T4MVC and the normal ActionLink approach which generate exactly the same link (unsurprisingly).
Summary
I have no idea why the Routes I have specified don't appear when trying to create an ActionLink in an area, so I was wondering whether anyone is able to point me in the direction of how I would be able to fix this and get the URL to look how I would like it to?

After a bit of playing around I managed to get it working. The way that I ended up fixing it was by calling AreaRegistration.RegisterAllAreas() from the RouteConfig.RegisterRoutes() after having called MapMvcAttributeRoutes()
public class RouteConfig
{
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 },
namespaces: new[] { "TrainingPortal.Controllers" }
);
}
}
As you're not allowed to call this method twice (or ASP.NET gets rather upset with you having registered the same route names twice) I removed the call to AreaRegistration.RegisterAllAreas() from Global.asax leaving it looking like this;
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
Unfortunately this alone didn't solve the problem, I also had to make a couple modifications to the Controller as well. The modifications were to add the RoutePrefix and RouteArea attributes to the Controller like this;
[RouteArea("PracticeAdmin")]
[RoutePrefix("Practice")]
public partial class PracticesController : Controller
{
This had the added benefit that when specifying the route for a particular action through the Route attribute you didn't have to specify those parts any more, so originally an action's signature would have looked like this;
// GET: PracticeAdmin/Practices/{practiceName}/Members
[Route("PracticeAdmin/Practices/{practiceName}/Members")]
public virtual ActionResult Members(string practiceName)
{
it would now look like this;
// GET: PracticeAdmin/Practices/{practiceName}/Members
[Route("{practiceName}/Members")]
public virtual ActionResult Members(string practiceName)
{
After making all those changes, the website is behaving as expected.

Related

MVC5 Url.Action not returning correct URL

I'm trying to use a Kendo Grid for a list of objects on my model, but the url's generated by the .Create() etc. methods are not generating the url correctly.
It doesn't appear to be just Kendo though because even in my controller using Url.Action() generates the wrong url.
// POST: Assessment/Create
[HttpPost]
[ValidateAntiForgeryToken]
[Route("eForms/Assessment/Create")] // <-- Tried with and without this
public ActionResult Create(AssessmentPoco model)
{
var x = Url.Action(("Allergy_Read", "Assessment");
}
//POST: Assessment/Allergy_Read
[HttpPost, ActionName("Allergy_Read")]
[Route("AllergyRead", Name = "Allergy_Read")]
public ActionResult Allergy_Read([DataSourceRequest] DataSourceRequest request, AssessmentAllergiesSection model) //, int id)
{
return Json(new[] { model }.ToDataSourceResult(request, ModelState));
}
Expected: eForms/Assessment/Allergy_Read
Actual: /?action=Allergy_Read&controller=Assessment
Route config:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("");
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//web forms default
routes.MapPageRoute(
routeName: "WebFormDefault",
routeUrl: "",
physicalFile:"~/default.aspx");
routes.MapRoute(
name: "API",
url: "eforms/api/{controller}/{action}/{id}",
defaults: new {controller="Customer", action="GetCustomers", id = UrlParameter.Optional}
);
////mvc default
routes.MapRoute(
name: "Default",
url: "eforms/{controller}/{action}/{id}",
defaults: new { controller = "IncidentReports", action = "Search", id = UrlParameter.Optional }
);
}
Not sure what else could be at fault here (besides my brain), any ideas?
Clarifications (from comments):
We are using Areas
Global.asax is calling RegisterRoutes (also tried turning it off no change)
Update:
This project is a newly added MVC project to an existing ASP.Net WebForms app. I updated the Route config because I was using looking at the wrong one.
You're using routeAttribute, so put a name inside it and use Html.RouteLink or Url.RouteUrl instead of Url.Action().
Example:
[Route("menu", Name = "mainmenu")]
public ActionResult MainMenu() { ... }
Usage in View:
Main menu
I tried this code in controller:
public class HomeController : Controller
{
[Route("AllergyRead", Name = "Allergy_Read")]
public ActionResult Allergy_Read()
{
return View();
}
}
And:
#Html.RouteLink("Allergy Read", "Allergy_Read")
Give me the right route to action. I can't figure out why your implementation isn't working.

Right way to Redirect a URL segment to MVC Controller

I have an existing Controller
public class HomeController : Controller
{
public ActionResult Index()
{
return Redirect("/Scorecard");
}
[OutputCache(Duration = 18000)]
public ActionResult Scorecard()
{
return View();
}
}
This currently Maps to http://siteurl/Home/Scorecard . I wanted to the segment http://siteurl/scorecard to redirect to this Controller Action . What would the best wayt to do this . I tried checking the RequestUrl in Session_Start in Global.aspx but the redirects dont seem to be happening . The other alternative I thought of was using a Different Controller like "ScorecardController" and then having a RedirectToAction("Scorecard","Home") in the Index view there.
you could add a FilterAccess class on your App_Start folder to do something like this:
public class FilterAcess : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//Redirect
if (HttpContext.Current.Request.Url=="http://siteurl/scorecard"){
context.HttpContext.Response.Redirect("~/Home/Scorecard");
}
}
}
RedirectToAction is better way to do it, because, in case you change routing table later, redirect URL will be in adapted.
public class HomeController: Controller
{
public ActionResult Index()
{
return RedirectToAction("Scorecard");
}
[OutputCache(Duration = 18000)]
public ActionResult Scorecard()
{
return View();
}
}
You should also update RouteTable with additional route, before "Default" route:
public static void RegisterRoutes(RouteCollection routes)
{
routes.LowercaseUrls = true;
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "NoHomeSegmentInUrl",
url: "{action}/{id}",
defaults: new { controller = "Home", id = UrlParameter.Optional });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
And, for lower case routes you need line routes.LowercaseUrls = true;

Web API 2 routing fails for literal segment

I'm facing an issue with Web API 2 routing for literal segments.
In one project, I have ASP.NET MVC and WebAPI2 running together, the project is also running MVC areas.
Under each area, there is API folder which contains APIs. I'm facing an issue when trying to request the following url:
{host}/accesscontrol/api/reporting/bookings.
accesscontrol here is the area name
reporting is the controller
bookings is a literal segment.
The error I'm getting:
No action was found on the controller 'Reporting' that matches the request.
This is the controller that should receive this request:
[RoutePrefix("accesscontrol/api/reporting")]
public class ReportingController : ApiController
{
[Route("bookings")]
[ResponseType(typeof(Booking))]
[HttpGet]
public async Task<IHttpActionResult> Bookings(string q = null)
{
//Code to get data
return Ok(bookings);
}
}
When I remove [Route('Bookings')] attribute, the request is working well regardless if Bookings segment is there or not.
This is the configuration of routing under area registration class:
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.MapHttpRoute(
"AccessControlApi_default",
"accesscontrol/api/{controller}/{id}",
new { id = RouteParameter.Optional }
);
context.MapRoute(
"AccessControl_default",
"accesscontrol/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
How can I let the app to understand those literal segments under areas?
Edit
I'm calling RegisterAllAreas in Global.asax.cs file, as follow:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
I managed to make it work just add action after your controller in your MapHttpRoute and change [Route("bookings")] attribute to [ActionName("bookings")] and everything works.
context.Routes.MapMvcAttributeRoutes();
context.Routes.MapHttpRoute(
"AccessControlApi_default",
"Accesscontrol/api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional});
I hope this will help you out.
I resolved my issue by moving: AreaRegistration.RegisterAllAreas(); from global.asax.cs file to RouteConfig.cs file.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] {"WebPortal.Controllers"}
);
}
}

Can I have a controller with the same name as an area?

Let's say I have an area named Admin in my MVC application. This area has a HomeController with an Index action like so:
using System.Web.Mvc;
namespace AreasPractice.Areas.Admin.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return Content("Admin.HomeController");
}
}
}
The area registration for the Admin area has the following defaults for the area related route:
using System.Web.Mvc;
namespace AreasPractice.Areas.Admin
{
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
//, new[] { "AreasPractice.Areas.Admin.Controllers" }
);
}
}
}
My application's root area also has a HomeController with an Index action and a route config like so:
using System.Web.Mvc;
namespace AreasPractice.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return Content("From home controller of the root");
}
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
, namespaces: new[] { "AreasPractice.Controllers" }
);
}
}
Now, I add an AdminController with an Index action in the root area of my application like so:
using System.Web.Mvc;
namespace AreasPractice.Controllers
{
public class AdminController : Controller
{
public ActionResult Index()
{
return Content("Root -> Admin Controller -> Index action.");
}
}
}
When I run the application, I am expecting to see something interesting, like, may be an exception to the effect, "I can't figure out what route you want."
But when I run the application, it runs just fine and a request for:
/Admin/
yields to the HomeController in the Admin area.
This is because, apparently, and as I recall, the routing mechanism works on a priority basis. It finds the first match and goes with it.
And it is apparently finding that the Admin_default route satisfies the request pattern even before it applies the Default route.
My question(s):
Is my understanding so far correct?
What do I do to play with it? What if I want it to go to the
AdminController in the root area of the application?
Okay, I got the answer after some more thinking. If I just moved the area registration in the global.asax file to after the default route has been registered, it now goes to the AdminController of my root instead of going to the Admin area's HomeController.
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
// Remove from here
// AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// Put here after the default route has been
// registered via the RouteConfig.RegisterRoutes
// call above
AreaRegistration.RegisterAllAreas();
}
}

ASP.NET MVC `Html.ActionLink` between "Areas"

I have added a new Area to my MVC3 project and I am trying to link from the _Layout page to the new Area. I have added an Area called 'Admin' that has a controller 'Meets'.
I used the visual studio designer to add the area so it has the correct area registration class etc, and the global.asax file is registering all areas.
However, when I use the following 2 action links in a page in the root, I run into a few problems:
#Html.ActionLink("Admin", "Index", "Meets", new { area = "Admin" }, null)
#Html.ActionLink("Admin", "Index", "Meets", new { area = "" }, null)
When clicking both links, I am taken to the Meets controller in the Admin area, where the application then proceeds to throw an error saying it cannot find the Index page (even though the Index page is present in the Views folder in the Area sub-directory.
The href for the 1st link looks like this:
http://localhost/BCC/Meets?area=Admin
And the href for the 2nd link looks like this:
http://localhost/BCC/Meets
Also if I hit the link that I expect to be created:
http://localhost/BCC/Admin/Meets
I just get a resource cannot be found error. All very perplexing! I hope someone can help...
Strange indeed. Steps that worked perfectly fine for me:
Create a new ASP.NET MVC 3 application using the default Visual Studio template
Add an area called Admin using Visual Studio designer by right clicking on the project
Add new Controller in ~/Areas/Admin/Controllers/MeetsController:
public class MeetsController : Controller
{
public ActionResult Index()
{
return View();
}
}
Add a corresponding view ~/Areas/Admin/Views/Meets/Index.cshtml
In the layout (~/Views/Shared/_Layout.cshtml) add links:
#Html.ActionLink("Admin", "Index", "Meets", new { area = "Admin" }, null)
#Html.ActionLink("Admin", "Index", "Meets", new { area = "" }, null)
Run the application.
Rendered HTML for the anchors:
Admin
Admin
As expected the first link works whereas the second doesn't.
So what's the difference with your setup?
Another option is to utilize RouteLink() instead of ActionLink(), which bypasses the area registrations altogether:
ActionLink version:
Html.ActionLink("Log Off", "LogOff", "Account", new { area = "" }, null)
RouteLink version:
Html.RouteLink("Log Off", "Default",
new { action = "LogOff", controller = "Account" })
The second parameter is a "Route Name" which is registered in Global.asax.cs and in various 'AreaRegistration' subclasses. To use 'RouteLink' to link between different areas, you only need to specify the correct route name.
This following example shows how I would generate three links to different areas from a shared partial, which works correctly regardless of which area I am 'in' (if any):
#Html.RouteLink("Blog", "Blog_default",
new { action = "Index", controller = "Article" })
<br/>
#Html.RouteLink("Downloads", "Download_default",
new { action = "Index", controller = "Download" })
<br/>
#Html.RouteLink("About", "Default",
new { action = "Index", controller = "About" })
Happy coding!
I figured this out - I created a new test project and did exactly the same thing I was doing before and it worked...then after further inspection of all things route-related between the two projects I found a discrepancy.
In the global.asax file in my BCC application, there was a rogue line of code which had inexplicably appeared:
public static void RegisterRoutes(RouteCollection routes)
{
// Problem here
routes.Clear();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
As you can see where my comment is, at some time or other I had placed the routes.Clear() call at the beginning of RegisterRoutes, which meant after I had registered the Areas in Application_Start, I was then immediately clearing what I had just registered.
Thanks for the help...it did ultimately lead to my salvation!
Verify that your AdminAreaRegistration class looks like this:
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
and that you have this in Global.asax.cs:
protected void Application_Start()
{
... // ViewEngine Registration
AreaRegistration.RegisterAllAreas();
... // Other route registration
}
I solved this problem by doing the following.
In my Global.asax.cs, I have
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{*favicon}", new { favicon = #"(.*/)?favicon.ico(/.*)?" });
}
protected void Application_Start()
{
//Initialise IoC
IoC.Initialise();
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
In my PublicAreaRegistration.cs (Public Area), I've got
public class PublicAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Public";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute("Root", "", new { controller = "Home", action = "Index" });
context.MapRoute(
"Public_default",
"Public/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
, new[] { "<Project Namespace here>.Areas.Public.Controllers" }
);
}
}
In my AuthAreaRegistration.cs (Area for Restricted access), I've got
public class AuthAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Auth";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Auth_default",
"Auth/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
And finally, my links in my *.cshtml pages would be like
1) #Html.ActionLink("Log Off", "LogOff", new{area= "Public", controller="Home"})
or
2) #Html.ActionLink("Admin Area", "Index", new {area= "Auth", controller="Home"})
Hope this saves someone hours of research! BTW, I'm talking about MVC3 here.
Kwex.
This might not be the case for most of the developers but I encountered this problem when I added a my first area and did not build my solution. As soon as I build my solution the links started to populate correctly.

Resources