Dec212009

Action’s parameters and Selection Decision

    When you have an action, with an overloaded version of it in the controller, its parameters are not used to to decide the selection.

Let’s assume you have a form, and user clicks on the submit button, to send the form to the server. The action that renders the view for the get request is: Register(), and the action that receives the request is Register(FormCollection collection) similar to something like this:

public ViewResult Register()
{
    //do some prep for the view
    return View();
}

public ViewResult Register(FormCollection collection)
{
    //do something with the collection
   // either return a view, or redirect to another action
}

I didn’t put any action selector on purpose; to prove my point. If you run this page, and try to call /Register you can get an error. Basically the framework is complaining that it can’t pick which Register() action it should call to respond /Register. You may think that because the second Register() action has a parameter, it should not be picked for a get request; however it is not the case :)

Let’s look at the MVC source code to see how an action is executed. If you open the source code, and check the Controller.cs file, around line 162, you will this function:

 

   1:    protected override void ExecuteCore() {
   2:              TempData.Load(ControllerContext, TempDataProvider);
   3:   
   4:              try {
   5:                  string actionName = RouteData.
GetRequiredString("action");
   6:                  if (!ActionInvoker.InvokeAction
(ControllerContext, actionName)) {
   7:                      HandleUnknownAction(actionName);
   8:                  }
   9:              }
  10:              finally {
  11:                  TempData.Save(ControllerContext, TempDataProvider);
  12:              }
  13:          }

 

Look carefully at line 6. This where the action is invoked, now we have to dig in more, and see what is happening inside ActionInvoker.InvokeAction method. This method is doing 1 thing that is important for the topic of this blog:

 ActionDescriptor actionDescriptor = FindAction(controllerContext,
controllerDescriptor, actionName);
 
 

It is trying to find the action to execute, given the action name, and the controller name. It adds the results into a list of methodinfo. Here is it:

   public MethodInfo FindActionMethod(ControllerContext controllerContext, 
string actionName) { List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods
(controllerContext, actionName); methodsMatchingName.AddRange(NonAliasedMethods[actionName]); List<MethodInfo> finalMethods = RunSelectionFilters(
controllerContext, methodsMatchingName); switch (finalMethods.Count) { case 0: return null; case 1: return finalMethods[0]; default: throw CreateAmbiguousMatchException(finalMethods,
actionName); } }


After it adds the methods that it finds to the list, it runs the RunSelectionFilters function to eliminate the methods. After the eliminations, if it has more than 1 method left in the list, it throws the exception; which you see when you call /Register. As you can see the parameters of the action has nothing to do with the selection.
One way to help the framework to pick the right action is to use SelectionFilters; as you can see from the code above, FindActionMethod() calls the RunSelectionFilter() to eliminate the actions.

If you use AcceptVerbs selection filter to differentiate between the two Register function, framework won’t be complaining as before; so this is one way of doing it right:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Register()
{
    //do some prep for the view
    return View();
}

[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(FormCollection collection)
{
    //do something with the collection
   // either return a view, or redirect to another action
}
 

This time, when there is a get request, RunSelectionFilter() will eliminate Register(FormCollection collection), as it has the AcceptVerbs post selecttionfilter.

I am still walking through the ASP.NET MVC source code, and really enjoying the way the developers wrote it; it is really fun to read it.

Have fun :)



Tags:

E-mail | Permalink | Trackback | Post RSSRSS comment feed 0 Responses

Dec052009

MVC Case Sensitivity & Action Names & NonAction Attribute

Today, it was the first class of our free ASP.NET MVC course. I was talking about URLs not being case sensitive. So that for example if you have the request “Home/About” this goes to HomeController and About action, as well as hOmE/AbOUT is going to the same controller and same action method.
One of the students ask me, what will happen if we have 2 about action methods in the same controller with different cases such as:

public class HomeController:Controller
{
....
    public ViewResult About()
    {
        return View();
    }

    public ViewResult aBOut()
    {
        return View();
    }
}

If you write the code above, and try to run the app, and call Home/About, here is the error message you will get:

image

The framework can’t determine which ‘about’ function to call, and throws the exception telling that the call is ambiguous. Of course one way to fix this problem is to change the action name. If for some reason you don’t want to change the action name, and one of these function is not an action, then you can decorate this non action method with NonAction attribute. Example:

[NonAction]
public ActionResult aBOut()
{
   return View();
}

So now this function is not anymore an action method that will handle the web requests. What is NonAction attribute?
It is an attribute that derives from ActionMethodSelectorAttribute. When you derive from this attribute you can override IsValidForRequest function, and return false, to refuse to serve as an action. If you check the MVC source code, here is the code for NonAction attribute:

[AttributeUsage(AttributeTargets.Method, 
AllowMultiple = false, Inherited = true)] public sealed class NonActionAttribute : ActionMethodSelectorAttribute { public override bool IsValidForRequest(ControllerContext
controllerContext, MethodInfo methodInfo) { return false; } }
 

This code is refusing to act as a request. So you can use this NonAction attribute on any of your public functions inside a controller to make them not web accessible.



Tags:

E-mail | Permalink | Trackback | Post RSSRSS comment feed 0 Responses