i have:
AlbumsController
PhotoRepository
Index.aspx (view)
inside of Index.aspx, i have a partial view call AlbumControl. I want to update this via ajax and ajaxhelper.
the issue is that i want the ability to do the following:
http://www.mysite.com/Albums
http://www.mysite.com/Albums?FilterTag=Birthdays
when i do this, i get the following error:
The current request for action 'Index' on controller type 'AlbumsController' is ambiguous between the following action methods:
System.Web.Mvc.ActionResult Index(System.String) on type Controllers.AlbumsController
System.Web.Mvc.ActionResult Index() on type Controllers.AlbumsController
i would have thought that asp.net mvc would have figured it out where if i pass in a parameter in the querystring it would go to the Index(string Tag) method and if i didn't pass in a parameter, it would go to Index().
suggestions?
The problem is that the MVC Routing Engine can't tell the difference between:-
1) Calling Index()
and
2) Calling Index(string tag) with tag = null
That's why it says the request is ambiguous.
You can just do:-
public ActionResult Index(string tag)
{
if(String.IsNullOrEmpty(tag))
{
// index code goes here
return View("Index");
}
else
{
// code to handle filtered view goes here
return View("Tag");
}
}
or you can force the parameter to be required with a custom attribute:-
ASP.NET MVC ambiguous action methods
or you can set up routes so that Albums and Albums?FilterTag=X explicitly go to different actions (Incidentally, I'd recommend "Albums" and "Albums/X"):-
routes.MapRoute("AlbumsIndex", "Albums",
new { controller = "Albums", action = "Index" });
routes.MapRoute("AlbumsTag", "Albums/{tag}",
new { controller = "Albums", action = "Tag", tag = "" });
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" );
and
public ActionResult Index() { ... }
public ActionRestlt Tag(string tag) { ... }
Related
WebAPI 2 intelligently handles the Async suffix on action methods. For example, if I create a default WebAPI project it will route to the correct action regardless of the suffix. I.e.:
http://host/api/controller/action - SUCCEEDS
http://host/api/controller/actionAsync - SUCCEEDS
However, if I create an equivalent controller using MVC 5 the behavior is different:
http://host/controller/actionAsync - SUCCEEDS
http://host/controller/action - FAILS - 404
The fact that it fails with a 404 when the Async suffix isn't present is surprising. Nevertheless, I tried to add a route to handle it, but it still fails:
routes.MapRoute(
name: "DefaultAsync",
url: "{controller}/{action}Async/{id}",
defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
Here's the MVC and WebAPI controllers that I used to test (based on a new MVC/WebAPI project with default routes):
public class SampleDto { public string Name; public int Age; }
public class SampleMvcController : Controller
{
public Task<JsonResult> GetJsonAsync()
{
// Illustration Only. This is not a good usage of an Async method,
// in reality I'm calling out to an Async service.
return Task.FromResult(
Json(new SampleDto { Name="Foo", Age = 42}, JsonRequestBehavior.AllowGet));
}
}
public class SampleWebApiController : ApiController
{
public Task<SampleDto> GetJsonAsync()
{
return Task.FromResult(new SampleDto {Name="Bar", Age=24});
}
}
As I'm in the middle of making a bunch of methods async, I'd prefer not to specify an action name. The routing documentation suggests that it can pick up literals which can separate segments, but I haven't had any luck yet.
UPDATE:
The problem is that the Action as retrieved by MVC contains the Async suffix, but no corresponding action (or action name) exists on the controller. The piece that matches the action, MyAction, doesn't identify MyActionAsync as a match.
In retrospect, that's why the route doesn't work. It attempts to identify the action as ending with Async but leave off the Async suffix from the action used in matching, which is not what I wanted to do. It would be useful in the event that I wanted to create only a MyAction method (that was async but didn't follow the Async naming convention) and have it map to the corresponding MyAction method.
Initially I was sorely disappointed that the AsyncController type accomplishes exactly what we're looking for out-of-the-box. But apparently it's old-hat and is only still around for "backwards compatibility with MVC3."
So what I ended up doing was making a custom AsyncControllerActionInvoker and assigning it to a custom controller's ActionInvoker. The CustomAsyncControllerActionInvoker overrides the BeginInvokeAction method to see if an action method ending in "Async" for the appropriate action exists (ex. you pass in "Index" it looks for "IndexAsync"). If it does, invoke that one instead, otherwise, continue on as you were.
public class HomeController : CustomController
{
public async Task<ActionResult> IndexAsync()
{
ViewBag.Header = "I am a page header."
var model = new List<int> {1, 2, 3, 4, 5};
await Task.Run(() => "I'm a task");
return View(model);
}
public ActionResult About()
{
return View();
}
}
public class CustomController : Controller
{
public CustomController()
{
ActionInvoker = new CustomAsyncControllerActionInvoker();
}
}
public class CustomAsyncControllerActionInvoker : AsyncControllerActionInvoker
{
public override IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state)
{
var asyncAction = FindAction(controllerContext, GetControllerDescriptor(controllerContext), $"{actionName}Async");
return asyncAction != null
? base.BeginInvokeAction(controllerContext, $"{actionName}Async", callback, state)
: base.BeginInvokeAction(controllerContext, actionName, callback, state);
}
}
Alas, navigating to /Home/Index properly calls the IndexAsync() method and serves up the Index.cshtml (not IndexAsync.cshtml) view appropriately. Routes to synchronous actions (ex. "About") are handled as normal.
Instead of trying to put a literal together with a placeholder, you should make a constraint to ensure your action name ends with Async.
routes.MapRoute(
name: "DefaultAsync",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "IndexAsync", id = UrlParameter.Optional },
constraints: new { action = #".*?Async" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
I am trying to create a RenderAction method on the Master page that will dynamically generate a side bar based on the current controller and action. When the RenderAction is called in the view it is populated with the controller and action of that particular RenderAction Method. For Example, if I am on the controller of “Home” and action of “Index” I am expecting "Home" and "Index" in the GenerateSideBar method. Instead I get the controller of “Home” and action of “RenderSideBar”. Thanks For your Help!
My Master Page View:
<div class="side-bucket">
<%
string Controller = ViewContext.RouteData.Values["Controller"].ToString();
string Action = ViewContext.RouteData.Values["Action"].ToString();
%>
<% Html.RenderAction("RenderSideBar", "Home", new { controller = Controller, action = Action }); %>
Controller Action:
public ActionResult GenerateSideBar(string controller, string action)
{
switch (controller.Trim().ToLower())
{
case "member" :
return View(#"sidebar\MemberSidebar");
default:
break;
}
return null;
}
The problem is that controller and action parameter names are special because used in your routes and will take precedence when the default model binder tries to set their values. Simply rename them like this:
public ActionResult GenerateSideBar(string currentController, string currentAction)
{
...
}
and render it:
<% Html.RenderAction("RenderSideBar", "Home",
new { currentController = ViewContext.RouteData.Values["controller"],
currentAction = ViewContext.RouteData.Values["action"] }
); %>
Now you should get the correct parent action name. Also if this action is intended to be used only as child action you could decorate it with the [ChildActionOnly] attribute.
Consider two methods on the controller CustomerController.cs:
//URL to be http://mysite/Customer/
public ActionResult Index()
{
return View("ListCustomers");
}
//URL to be http://mysite/Customer/8
public ActionResult View(int id)
{
return View("ViewCustomer");
}
How would you setup your routes to accommodate this requirement?
How would you use Html.ActionLink when creating a link to the View page?
In global.asax.cs, add following (suppose you use the default mvc visual studio template)
Route.MapRoute("Customer",
"Customer/{id}",
new { Controller = "CustomerController", action="View", id="" });
Make sure you put this route before the default route in the template
You then need to modify your controller. For the view,
public ActionResult View(int? id)
{
if (id == null)
{
return RedirectToAction("Index"); // So that it will list all the customer
}
//...The rest follows
}
For your second question, ActionLink is simple.
Html.ActionLink("Link Text", "View", "Customer", new {id=1}, null);
I have few pages that quite similar to the others but one of them doesn't work.
When I write 'http://localhost:2265/Segment/' I get annoying error message "Server Error in '/' Application.
The resource cannot be found."
Other pages like 'http://localhost:2265/User/' works very well AND also 'http://localhost:2265/Segment/Create'. So Index of the Segment is the problem. I have used ASP.NET Routing Debugger and on other pages I get correct mappings, but I get this same error message "resource cannot be found" also when using debugger.. I think this indicates that Default route doesn't catch it either..
Any ideas?
Here is my MapRoute commands.
routes.MapRoute(
"Maintenance",
"Maintenance/{action}",
new { controller = "Maintenance", action = "Index", id = "" }
);
routes.MapRoute(
"User",
"User/{action}",
new { controller = "User", action = "Index", id = "" }
);
routes.MapRoute(
"Segment",
"Segment/{action}",
new { controller = "Segment", action = "Index", id = "" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
Update:
Thank you for the quick reply!
I removed all routes except the default. It didn't solve the problem, but now the route list is shorter.
I have other classes inside my controller file like this:
public class SegmentFormViewModel
{
}
public class SegmentController : Controller
{
}
public class SegmentFormCreateModel : Segment
{
}
I tried to move it inside controller, but that didn't help either.
Is there any way to debug this problem?
Update:
Here is my controller (without contents of the methods)
public class SegmentController : Controller
{
//
// GET: /Segment/
public ActionResult Index()
{
}
//
// GET: /Segment/Details/5
public ActionResult Details(Guid id)
{
}
//
// GET: /Segment/Create
public ActionResult Create()
{
}
//
// POST: /Segment/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
{
}
//
// GET: /Segment/Edit/5
public ActionResult Edit(int id)
{
}
//
// POST: /Segment/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection)
{
}
}
To match your /Segment/ route you will need a "SegmentController" controller class to have an "Index" action that accepts GET requests, ie not restricted with [AcceptVerbs(HttpVerbs.Post)].
As Darin has already commented, the default route would handle all the other routes you have added so you don't need the extra routes.
After update:
Your problem is probably more to do with the actions in the Segment controller. It doesn't matter what classes you have in which file. What actions do you have in the Segment controller?
After 2nd update:
Your actions look ok so I suspect the problem is in code you have not listed.
Next steps:
1. Use the router debugger already mentioned.
2. Download the MVC source so you can step through it.
Last resort: Start a brand new project and only add the Segment controller. Then keep adding related code until you find the problem.
Use Phil Haack's RouteDebugger. It'll tell you what was matched, which often clears the problem up pretty quickly.
It's really easy to set up: drop RouteDebug.dll in your /bin folder and make this change to your App Start event:
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
}
How can I retrieve a site-wide URL parameter in a route without cluttering each controller action with a parameter? My question is similar to this question, but I want to avoid the ModelBinder clutter. Ie. in Global.asax.cs:
routes.MapRoute(
"Default", // Route name
"{sitename}/{controller}/{action}/{id}",
new { sitename = "", controller = "SomeController", action = "Index", id = "" } );
So, instead of the following in SomeController class:
public ActionResult Index(string sitename)
{
SiteClass site = GetSite(sitename);
...
return View(site.GetViewModel());
}
I would rather have the following:
public ActionResult Index()
{
SiteClass site = CurrentSite; // where CurrentSite has already retrieved data based on unique URL sitename parameter.
...
return View(site.GetViewModel());
}
Perhaps this can be achieved with controller-wide action filter? OnActionExecuting?
First add a route to Global.aspx.cs to pass a {sitename} parameter:
routes.MapRoute(
"Sites", // Route name
"{sitename}/{controller}/{action}/{id}", // URL with parameters
new { sitename = "", controller = "Home", action = "Index", id = "" } // Parameter defaults
);
Then add the following simple code inside a base controller:
public class BaseController: Controller
{
public string SiteName = "";
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpRequestBase req = filterContext.HttpContext.Request;
SiteName = filterContext.RouteData.Values["sitename"] as string;
base.OnActionExecuting(filterContext);
}
}
And use in your derived controller:
public class HomeController: BaseController
{
public ActionResult Index()
{
ViewData["SiteName"] = SiteName;
return View();
}
}
Perhaps I misunderstand the question, but why not simply do the following inside your controller action:
var sitename = RouteData.Values["sitename"] as string;
I don't understand why you would need to override OnActionExecuting (per #pate's answer) when you can retrieve the value when you need it.
There's also no need to create a base class and have everything derive from it. If you object to copying that line into every action method of every controller, why not create an extension method?