While reading about mcv3 I came across an attribute name called [ActionName]. It actually gives a new name to the action method. I tested a scenario which made me think; how are the internals working. When I have the following two action methods in my controller class
[ActionName("Test")]
public ActionResult Index()
{
return View();
}
[ActionName("Index")]
public ActionResult Test()
{
return View();
}
I thought this will end up in some kind of infinite loop or will give some ambiguity exception. But the same works fine and the second method is called when i give this url http://mysite:1234/mycontroller
What made MVC engine to choose the second method and not the first?
Any idea why this happens?
Phil Haack has a post on this matter: How a method becomes an action
In short: the ControllerActionInvoker uses reflection to find a method matches the action name.
The ActionNameAttribute redefines the name of the method.
Also be aware that the name of your View matches the ActionName, not the MethodName: the method Index will search for a view with the name "Test"
This is the magic of the Routing engine. Somewhere within the global.asax.cs file there would be routing patterns defined, mostly which defaults to
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
This is a routing pattern defined for your application. The action name attribute maps to the 'action' parameter within the parameter collection (3rd parameter for MapRoute).
In your case if you map the action 'Index' to method 'Test'. It should call Test() method. I am not sure whether it is still calling Index() for you. In fact the routing engine does not care about the method name if it finds the ActionName attribute over your public method.
ActionNameAttribute it represents an attribute that is used for the name of an action. If it is not present the name of the method is used.
Related
Being kind of a newb to MVC 4 (or really any of the MVC's for ASP.NET) I cant help but feel theres more to the URL helper than what I'm seeing.
Basically I've read the tutorials on populating the attributes in a controllers methods using a query string in the URL.
I dont liek query strings though and prefer a sectioned "folder" like style.
Without much further adu, this is the sample URL:
http://something.com/DataTypes/Search/searchString
this approach is actually pretty safe as there will only ever be single worded searches
I have tried in the DataTypes controller
[HttpGet]
public ActionResult Search(String q)
{
ViewBag.ProvidedQuery = q;
return View();
}
and a few other small variations, right now im just trying to get the string to show up in the view but I dont seem to be getting anything there.
Is there anyway to inject the 3rd string in the url into an attribute?
If not, which URL helper class am I supposed to use to acquire the string data in the URL? Even if I have to parse the whole URL manually so be it, i just need to acquire that 3rd element in the URL as a string
Extremely n00b question im sure, but either im not finding this simple guide somewhere, or im not searching google correctly...
What you're missing is that the default route parameter name is "id". So you want to do this:
[HttpGet]
public ActionResult Search(String id)
{
ViewBag.ProvidedQuery = id;
return View();
}
If you don't want to use the variable name id, then you can modify your Route to look like this:
routes.MapRoute(
name: "Search",
url: "DataTypes/Search/{searchString}",
defaults: new { controller = "DataTypes", action = "Search",
searchString = UrlParameter.Optional });
If you don't want the string to be optional, then you can remove the last field from the defaults object.
you can use RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext)) to get the routedata
String URL to RouteValueDictionary
You need to look at the routing in the Global.asax.cs. For example for your case you could add a route to the routes collection like this:
routes.MapRoute("Search",
"DataTypes/Search/{q}",
new { controller = "DataTypes", action = "Search" }
);
Then the q parameter will automatically get mapped to your action. The default controller mapping is likely mapping it to "id".
Suppose I have defined route like that,
context.MapRoute(
"Preview",
"/preview/{id}/{type}",
new { controller = "Preview", action = "Invoice", id = UrlParameter.Optional, type = UrlParameter.Optional }
);
I have controller with action Invoice
public ActionResult(int id, string type)
{
if (type == "someType")
{
// ...
}
else
{
// ..
}
}
I want to get rid of If-Else case inside the action. Is it possible to attribute action somehow, so ASP.MVC would distinguish between both, like:
Just a pseudocode tho show idea?
[HttpGet, ActionName("Action"), ForParameter("type", "someType")]
public ActionResult PreviewSomeType(int id) {}
[HttpGet, ActionName("Action"), ForParameter("type", "someType2")]
public ActionResult PreviewSomeType2(int id) {}
Is something like that possible in MVC2/3 ?
Action method selector
What you need is an Action Method Selector that does exactly what you're describing and are used exactly for this purpose so that's not a kind of a workaround as it would be with a different routing definition or any other way. Custom action method selector attribute is the solution not a workaround.
I've written two blog posts that will get you started with action method selection in Asp.net MVC and these kind of attributes:
Improving Asp.net MVC maintainability and RESTful conformance
this particular post shows an action method selector that removes action method code branches what you'd also like to accomplish;
Custom action method selector attributes in Asp.net MVC
explains action method selection in Asp.net MVC to understand the inner workings of it while also providing a selector that distinguishes normal vs. Ajax action methods for the same request;
I think thats what routing is for.
Just split your single route into two:
context.MapRoute(null, "preview/{id}/SomeType",
new {
controller = "Preview",
action = "Action1",
id = UrlParameter.Optional,
}
);
context.MapRoute(null, "preview/{id}/SomeOtherType",
new { controller = "Preview",
action = "Action2",
id = UrlParameter.Optional,
}
);
Or maybe more general, use the last segment as the action name:
context.MapRoute(null, "preview/{id}/{action}",
new { controller = "Preview",
id = UrlParameter.Optional,
}
);
But i think, optional segments have to appear at the end. So why don't you use the default routing schema
controller/action/id
?
We need to present the same site, with a different theme (and different data) depending on the "route" into the site.
www.example.com/Trade
www.example.com/Public
NB: These are not dynamic themes which the user can pick. Certain customers will be coming via a third party link which will always point them to one of the urls.
The value trade/public also needs to be used in queries from the UI to the database (to pull back varying data depending on the route into the site).
So what are my options?
Create custom view engine which uses the querystring (mvc route param) value to load the relevant master page.
In each controller action, grab the parameter (trade/public etc) and pass it off to the database queries.
public ActionResult List(string siteType){
products.ListFor(siteType);
}
The problem here is having to alter every controller action to pass the querystring value through.
This also presents an issue with any route defined in global.asax having to accept the parameter.
I'm wondering if there's another way, perhaps some combination of a custom controller base and host name e.g. trade.example.com, public.example.com?
First define routing:
routes.MapRoute(
"Default", // Route name
"{siteType}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
and then use it in controller:
RouteData.Values["siteType"]
How about this:
Create a base controller that all your controllers inherit from
Add a property: public string SiteType { get; protected set; }
Add an OnActionExecuting method to set it before any action is called, e.g.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//use filterContext to set SiteType somehow, e.g. you can look at the URL or the route data
}
Could someone explain how routes are associated with controllers in MVC 2? Currently, I have a controller in /Controllers/HomeController.cs and a view /Home/Index.aspx.
My route registration method looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
routes.MapRoute(
"Default",
// Route name
"{controller}/{action}/{id}",
// URL with parameters
new { controller = "Home", action = "Index", id = "" }
// Parameter defaults
);
}
If I request the URL: http://localhost/Home/Index, then the request is correctly handled by HomeController.Index().
However, for the life of me, I can't figure out how the url /Home/Index gets pointed to HomeController. The view aspx doesn't, as far as I can tell, reference HomeController, HomeController doesn't reference the view, and the route table doesn't explicitly mention HomeController. How is this magically happening? Surely I'm missing something obvious.
then
This is the convention in ASP.NET MVC.
When using the DefaultControllerFactory this convention is buried inside the internal sealed class System.Web.Mvc.ControllerTypeCache (typical for Microsoft to write internal sealed classes). Inside you will find a method called EnsureInitialized which looks like this:
public void EnsureInitialized(IBuildManager buildManager)
{
if (this._cache == null)
{
lock (this._lockObj)
{
if (this._cache == null)
{
this._cache = GetAllControllerTypes(buildManager).GroupBy<Type, string>(delegate (Type t) {
return t.Name.Substring(0, t.Name.Length - "Controller".Length);
}, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>(delegate (IGrouping<string, Type> g) {
return g.Key;
}, delegate (IGrouping<string, Type> g) {
return g.ToLookup<Type, string>(t => t.Namespace ?? string.Empty, StringComparer.OrdinalIgnoreCase);
}, StringComparer.OrdinalIgnoreCase);
}
}
}
}
Pay attention how the grouping is made. So basically the DefaultControllerFactory will look inside all the referenced assemblies for types implementing the Controller base class and will strip the "Controller" from the name.
If you really want to dissect in details ASP.NET MVC's pipeline I would recommend you this excellent article.
The default views engine that comes with ASP.NET MVC works on the following conventions:
You have a folder structure like this:
- Controllers\
|- HomeController.cs
- Views\
|- Home\
|-- Index.aspx
|- Shared\
When a request comes in, and matches a route defined in the RegisterRoutes method (see things like URL routing for more on that), then the matching controller is called:
routes.MapRoute(
"Default", // Route name, allows you to call this route elsewhere
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
In the default route, you are also specifying a default controller (without the "Controller" suffix) - the routing engine will automatically add Controller onto the controller name for you - and a default action.
In your controller, you call the simple method:
public ActionResult Index(){
return View();
}
The default view engine then looks for an aspx file called Index (the same as the action) in a folder called "Home" (the same as the controller) in the "Views" folder (convention).
If it doesn't find one in there, it will also look for an index page in the Shared folder.
From the ASP.NET MVC Nerd Dinner sample chapter
ASP.NET MVC applications by default use a convention-based directory naming structure when resolving view templates. This allows developers to avoid having to fully-qualify a location path when referencing views from within a Controller class. By default ASP.NET MVC will look for the view template file within the \Views\[ControllerName]\ directory underneath the application.
The \Views\Shared subdirectory provides a way to store view templates that are re-used across multiple controllers within the application. When ASP.NET MVC attempts to resolve a view template, it will first check within the \Views\[Controller] specific directory, and if it can’t find the view template there it will look within the \Views\Shared directory.
When it comes to naming individual view templates, the recommended guidance is to have the view template share the same name as the action method that caused it to render. For example, above our "Index" action method is using the "Index" view to render the view result, and the "Details" action method is using the "Details" view to render its results. This makes it easy to quickly see which template is associated with each action.
Developers do not need to explicitly specify the view template name when the view template has the same name as the action method being invoked on the controller. We can instead just pass the model object to the View() helper method (without specifying the view name), and ASP.NET MVC will automatically infer that we want to use the \Views\[ControllerName]\[ActionName] view template on disk to render it.
Edit to add:
Some example routes I've set up, that explicitly set the controller are:
routes.MapRoute(
"PhotoDetailsSlug",
"Albums/{albumId}/{photoId}/{slug}",
new {controller = "Albums", action = "PhotoDetails"},
new {albumId = #"\d{1,4}", photoId = #"\d{1,8}"}
);
Here I'm explicitly stating that I'm using the Albums controller, and the PhotoDetails action on that, and passing in the various ids, etc to the that action.
Inside the action Index there is a statement return View(). When a blank View is returned, the DefaultViewEngine searches several default folders for the name of the Controller method(specifically inside the FindView method). One of them is the Views/Home directory because Home is the name of the controller. There it finds the Index file, and uses it to display the result.
In my Asp.net MVC app, I have two methods on a controller, one for when the user first arrives on the view and then one when they submit the form on said view.
public ActionResult Foo() {}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Foo(string id, Account accountToFoo) {}
In the second action, there's a custom model binder that's assembling the account object that I'm acting on, though that's really not important. This all works fine in testing locally on a server.
We try to be pretty good about writing unit tests to test all our different views are properly getting routed to, including those that are HTTP POST. To do so, we've been using mvccontrib's test helper.
Testing gets have been super simple
"~/account/foo/myusername".
Route().
ShouldMapTo<AccountController>(c => c.Foo("myusername"));
My question is in testing POST routes, how do I write the lambda that I would use to verify the post is receiving accurate values, similar to the GET test above?
For a POST, it looks something like:
"~/account/foo".
WithMethod(HttpVerbs.Post).
ShouldMapTo<AccountController>(a => something_something);
It's the something_something portion of my lambda that I'm having trouble with. Using arbitrary values doesn't work ("a => a.Foo(0, new Account()"). How would I specify the expected values as part of the test?
EDIT I was hoping there was something akin to the way Moq has lambdas for statements such as foo.Setup(s => s.Foo(It.IsAny(), It.Is(i => i > 32)) and so on. Even I have to explicitly supply the values, that's workable--I just can't seem to grok the desired structure to pass those explicit values.
Here's an example. Assuming you have the following action:
public AccountController : Controller
{
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Foo(string id)
{
return View();
}
}
And the following route registered:
RouteTable.Routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "home", action = "index", id = "" }
);
You could test it like this:
var routeData = "~/account/foo".WithMethod(HttpVerbs.Post);
routeData.Values["id"] = "123";
routeData.ShouldMapTo<AccountController>(c => c.Foo("123"));
Some tweaking might be necessary to include the second Account argument you have.
Using the mvccontrib helper syntax:
"~/account/foo".WithMethod(HttpVerbs.Post).ShouldMapTo<AccountController>(a => a.foo(null));
You pass null as the Foo(string id, Account accountToFoo) method is never executed as part of the routing test.