#Html.Action throws InvalidOperationException for ChildOnlyAction in controller using RouteAttributes - c#

It is my understanding that child actions execute using the same routes as the parent actions but I am having difficulty getting this to work. I have an MVC5 application that incorporates several areas. In one area I want to display a partial with some data in every view that is separate from the controller model so I added a child action to accomplish this, but when I try to view any of the pages in that area I get an InvalidOperationException telling me that
No route in the route table matches the supplied values
The area controller looks like this (simplified for brevity):
[RouteArea("SomeArea")]
[RoutePrefix("Here")]
public class PageController : Controller
{
private readonly IRepository repository;
public PageController(IRepository Repository)
{
this.repository = repository;
}
[Route("Page")]
public ActionResult ShowPage(int pageNumber = 1)
{
var model = new PageViewModel(repository, pageNumber);
ViewBag.Title = "This is a Page";
return View("ShowPage", model);
}
[ChildActionOnly]
public PartialViewResult DataView()
{
var model = new DataViewModel(repository);
return PartialView("DataView", model);
}
}
The layout for the area:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#Scripts.Render("~/bundles/pages")
<body>
<div id="content">
<br />
#Html.Partial("~/Areas/Page/Views/Shared/Search.cshtml")
#RenderBody()
#Html.Action("DataView", "Page")
</div>
</body>
The route config:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
AreaRegistration.RegisterAllAreas();
routes.MapRoute("Default",
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
}
I have tried catching unhandled routing exceptions as in this article by David Boike to no avail.
Any suggestions would be greatly appreciated at this point. Thanks

Answer, supplied by Kiran in the comment above, is to simply decorate the child action with the route value for that action. Thank you Kiran!
I spent an ungodly amount of time researching this, and getting frustrated while I was at it, for such a simple solution. The routing now works with the child action as below:
[ChildActionOnly]
[Route("DataView")]
public PartialViewResult DataView()
{
var model = new DataViewModel(repository);
return PartialView("DataView", model);
}

Related

MVC route not kept on RedirectToAction

I've got this route configuration:
routes.MapRoute(
"routeB",
"routeB/{action}/{id}",
new { controller = "Sample", action = "IndexB", id = UrlParameter.Optional });
routes.MapRoute(
"routeA",
"routeA/{action}/{id}",
new { controller = "Sample", action = "IndexA", id = UrlParameter.Optional });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
And my Sample controller contains these Action methods:
public ActionResult IndexA(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexA()
{
return RedirectToAction("Confirmation");
}
public ActionResult Confirmation()
{
return View("Confirmation");
}
public ActionResult IndexB(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexB()
{
return RedirectToAction("Confirmation");
}
If I land on the localhost/RouteA page and make a POST (via a button) it redirects me to localhost/RouteB/Confirmation.
How can I get the page to redirect to the RouteA/Confirmation page?
Thanks.
There are 2 issues here. As others have pointed out, your HTTP POST needs to be corrected. Since you are sharing a single view for 2 different actions, the simplest way to do that is to set the actionName parameter to null. This tells MVC to use the action name from the current request.
#{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; }
<h2>Index</h2>
#using (Html.BeginForm(null, "Sample", new { id = "OrderForm" }))
{
#Html.AntiForgeryToken()
<button type="submit" id="orderFormBtn">Extend my discount.</button>
}
The second issue is that the RedirectToAction call is ambiguous between routeA and routeB when generating the URL. Since the first match always wins, the URL you are redirecting to is always the top one in your configuration.
You can fix this problem by using RedirectToRoute to specify the route name (in addition to your existing matching criteria) explicitly.
public class SampleController : Controller
{
public ActionResult IndexA(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexA()
{
return RedirectToRoute("routeA", new { action = "Confirmation" });
}
public ActionResult Confirmation()
{
return View("Confirmation");
}
public ActionResult IndexB(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexB()
{
// Note that changing this one to RedirectToRoute is not strictly
// necessary, but may be more clear to future analysis of the configuration.
return RedirectToRoute("routeB", new { action = "Confirmation" });
}
}
Your problem is quite simple.
Take a look to your controller, the methods IndexA and IndexB, returns the same view; (from your comments)
#{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; }
<h2>Index</h2>
using (Html.BeginForm("IndexB", "Sample", new { #id = "OrderForm" })) {
#Html.AntiForgeryToken()
<button type="submit" id="orderFormBtn">Extend my discount.</button>
}
When you click on submit, you always do a post to IndexB
There are a lot of ways to correct such a simple error, for example you simple could use 2 different views IndexA and IndexB and change
using (Html.BeginForm("IndexB", "Sample", new { #id = "OrderForm" })) {
to
using (Html.BeginForm("IndexA", "Sample", new { #id = "OrderForm" })) {
The problem is that both your routeA and routeB routes will work for creating a link for an IndexA action in SampleController. As a result, BeginForm will just short-circuit and pick the first route that will work, which may or may not be the "correct" one you're looking for. To differentiate the routes, generate them via the route name:
#using (Html.BeginRouteForm("routeA", new { #id = "OrderForm" })) {
However, that will only ever let you get the "default" routeA route, i.e. /routeA/. There's no way to specify a different action other than the default one for the route.
For more flexibility, you can employ attribute routing, which will allow you to give a custom route name to each action, which you can then use to get the URL for that action.
Short of that, you will need to differentiate the two routes in some way, so that it's not ambiguous which should be used when generating a URL. That's often difficult to do with standard routing, which is why attribute routing is the much better approach if you're going to diverge from simply using the default route for everything.
Alternatively, you can restructure your project to keep the same URL structure, but make it much easier to differentiate routes. By employing areas, you can then specify the area where the route should be created from. For example, assuming you had RouteA and RouteB areas, you could then do:
#using (Html.BeginForm("IndexB", "Sample", new { area = "RouteA", #id = "OrderForm" })) {
That would then mean having a SampleController in each area, but you can employ inheritance to reuse code.

Routing for Particular Action Adds 'Home' In Front of Action

I'm having an odd issue with routing in a basic ASP/MVC project.
I have a bunch of nav items setup:
<li>Home</li>
<li>Fire</li>
<li>Law Enforcement</li>
<li>Forensics</li>
<li>Reconstruction</li>
They all work fine, except for the third one, labeled law-enforcement.
When I mouse over this item, the URL is: http://localhost:54003/home/law-enforcement
When I mouse over any other item, the URl is: http://localhost:54003/fire
My Controller setup is:
public ActionResult Index()
{
return View();
}
[Route("~/fire")]
public ActionResult Fire()
{
return View();
}
[Route("~/law-enforcement")]
public ActionResult Law()
{
return View();
}
[Route("~/forensics")]
public ActionResult Forensics()
{
return View();
}
[Route("~/reconstruction")]
public ActionResult Reconstruction()
{
return View();
}
And my route config is:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapMvcAttributeRoutes();
routes.MapRoute("Default", "{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
}
When I go to the route the URL specifies, ASP responds with a 404 page not found (as it should). If I go to the route that I know it should be, such as localhost/law-enforcement then the correct View is rendered.
Any ideas why ASP is routing this one particular action incorrectly?
The Razor Url.Action(...) cannot refer to the route defined by the [RouteAttribute] on the controller action; instead it needs to refer to the action's name. So changing my Razor syntax to refer to #Url.Action("law", "home") instead of #Url.Action("law-enforcement", "home") solved the issue.
You can keep your url mapping in either of 2 ways as below:
One way is to decorate your actionresult with attribute as below:
// eg: /home/show-options
[Route("law-enforcement")] //Remove ~
public ActionResult Law()
{
return View();
}
As per docs
otherwise
Just add one more configuration in Route.config file
routes.MapRoute("SpecialRoute", "{controller}/{action}-{name}/{id}",
new {controller = "Home", action = "law-enforcement", id = UrlParameter.Optional}
);
Source

Displaying View of MVC in browser that dont have controller or action

In MVC, I have created a .cshtml file in the path "App/Main/Views".
I dont have any controller or Action for this file. But I need to display the content of .cshtml file in browser.
Please, let me know how I should give route path in 'RouteConfig.cs' file to achieve the above scenario.
Asp.Net MVC is based around Controllers that return views. Your routes point to Actions in Controllers that render Views. What you seem to be looking for is a static view which in this case can be handled as a pure *.html-file instead.
If you have no Controller or Action, it shouldn't be a view.
You can try this configuration
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 = "Main", action = "Index", id = UrlParameter.Optional }
);
}
}
And then in your MainController return that view this way:
public class MainController : Controller
{
// GET: Main
public ActionResult Index()
{
return View("MyView", new MyViewModel{ title = "blabla"});
}
}

Html.BeginForm rendering with "/" action

I have the following route defined
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Account", action = "Login", id = UrlParameter.Optional }
);
I am then trying to use Html.BeginForm as below
<% using (Html.BeginForm("Login", "Account", System.Web.Mvc.FormMethod.Post, new { #class = "login-form" }))
{ %>
But this renders me a form like below
<form class="login-form" action="/" method="post">
</form>
However if i change my defaults on me route to be something different like
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Login", id = UrlParameter.Optional }
);
My form then renders correctly, for info i am using Html.BeginForm() in a partial view that is returned from the login method on my account controller.
public class AccountController : Controller
{
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(LoginModel model)
{
if (ModelState.IsValid)
{
//TODO: Login user
}
return View(model);
}
}
The behavior that you are noticing is expected and is also correct. When generating links MVC goes through the list of routes in the route collection(top-down) and sees which routes can match based on the route data that you are providing in Html.BeginForm. For example, you can imagine a request coming in like POST / and in this case your Account controller's Login action would be called because of the presence of defaults.
Actually it is expected behaviour. Actually routing system is pretty clever and it knows the request which is coming that is for the default values.(In your case default controller is Account and default action is Login and in your begin form you are using the same controller and action).
So routing system will replace it
by '/'.
You can verify it by just adding one another controller let say Admin and the same View Login. And now just replace the controller by new controller like
<% using (Html.BeginForm("Login", "Admin", System.Web.Mvc.FormMethod.Post,
new { #class = "login-form" }))
Now you will have link like
<form class="login-form" action="/Admin" method="post"></form>
There will be no action, because routing system will find the action is default action.
Thanks

ASP MVC4 Finding appropriate method in controllers with the same name in different areas

I have seen some similar questions but i found that i have a diferent problem.
I have a MVC4 project with controllers in the base project and 3 areas. Each area contains a controller named AdminController and a method List.
My problem is when i use a link to go to method List always go through the same controller (e.g. AdminController in Users area) but the view returned is correct.
The structure of the project is like this:
Mimbre.Administration (web mvc4 project)
Areas
Contents
Controllers
AdminController
Models
Views
Admin
List.chtml
-ContentsAreaRegistration.cs
Logs
Controllers
AdminController
Models
Views
Admin
List.chtml
LogsAreaRegistration.cs
Users
Controllers
AdminController
Models
Views
Admin
List.chtml
UsersAreaRegistration.cs
Controllers (base project)
Views (base project)
Classes for area registration contains RegisterArea method like this (with apropiate name and namespace according to the current area):
public override void RegisterArea(AreaRegistrationContext context) {
context.MapRoute(
"Contents_default",
"Contents/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "Mimbre.Administration.Areas.Contents.Controllers" }
);
}
Global.asax contains method for register routes for base project
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 string[]{"Mimbre.Administration.Controllers"}
);
}
In index.chtml (in base project) I created links to List method of each area:
<a href="#Url.Action("List", "Admin", new { area = "Contents"})" >Contents list</a>
<a href="#Url.Action("List", "Admin", new { area = "Logs"})" >Logs list</a>
<a href="#Url.Action("List", "Admin", new { area = "Users"})" >Users list</a>
In generated html these links appear like this:
Contents list
Logs list
Users list
I think these links are correct, the problem is that any link clicked always takes me through method List in AdminController of Users area, method List in AdminController of another areas are never picked.
This problem could be solved by renaming the name of the controller, unique name for each area, but i really need keep the same controller name in all areas, any idea???
Thans in advance!!!
Finally I could simulate your problem. The only way that I could have the same output as you (doens't matter what area you're trying to access, but reaching same users controller and responding with right view) is when you register the Area with the wrong controllers namespaces.
/Admin/Home/List route:
namespace SandboxMvcApplication.Areas.Admin.Controllers
{
public class HomeController : Controller
{
public ActionResult List()
{
ViewBag.Message = "You've acessed Admin/List";
return View();
}
}
}
/Admin/Home/List view:
#{
ViewBag.Title = "List";
}
<h1>Admin</h1>
<h2>#ViewBag.Message</h2>
Admin Area Registration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "SandboxMvcApplication.Areas.Admin.Controllers" }
);
}
/User/Home/List route:
public class HomeController : Controller
{
public ActionResult List()
{
ViewBag.Message = "You've accessed User/List";
return View();
}
}
/User/Home/List view:
#{
ViewBag.Title = "List";
}
<h1>User</h1>
<h2>#ViewBag.Message</h2>
User Area registration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"User_default",
"User/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "SandboxMvcApplication.Areas.Admin.Controllers" }
);
}
With this configuration, my outputs are:
/Admin/Home/List
Admin
You've acessed Admin/List
/User/Home/List
User
You've acessed Admin/List
Note that the first word works fine (insuring that the right view was rendered but with wrong data comming from list action).
If I correct the user controller namespace in UserAreaRegistration ("SandboxMvcApplication.Areas.User.Controllers"), everything works fine!
Check it out if this could help you or try to explicitly put your namespaces to the right controller. You could also check if the Admins controller are with the right namespace too.
You need to append one more setting to your route registration. This will prevent the DefaultControllerFactory from searching for controller in unintended areas.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[]{"Mimbre.Administration.Controllers"}
).DataTokens["UseNamespaceFallback"] = false;

Categories

Resources