.NET MVC4 ActionNameSelectorAttribute multiple but

2019-01-20 17:52发布

问题:

I have read many posts about allowing different controller actions for different view-buttons. However, I cannot get this to work.

I use this code-snippet obtained from http://blog.ashmind.com/2010/03/15/multiple-submit-buttons-with-asp-net-mvc-final-solution/.

public class HttpParamActionAttribute : ActionNameSelectorAttribute 
{
    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo) 
    {
        if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
            return true;

        if (!actionName.Equals("Action", StringComparison.InvariantCultureIgnoreCase))
            return false;

        var request = controllerContext.RequestContext.HttpContext.Request;
        return request[methodInfo.Name] != null;
    }
}

When I step through this code, I see a compare of actionName with methodInfo.Name. How can these EVER be equal when the whole purpose is to name the method different from the controller's action.

What is the return value of true or false actually mean as to the behavior/functionality?

Should I be overriding the 'fciContactUs' action with 'Action'?

The Controller = "HomeController"

  [HttpParamAction]
  [ActionName("Action")]
  [AcceptVerbs(HttpVerbs.Post)]
  [ValidateInput(false)]
  public ActionResult DoClearForm(fciContactUs p_oRecord)
  {
     return RedirectToAction("fciContactUs");
  }

  [HttpParamAction]
  [ActionName("Action")]
  [AcceptVerbs(HttpVerbs.Post)]
  public ActionResult TrySubmit(fciContactUs p_oRecord)
  {
     // This must be the Submit command.
     if (ModelState.IsValid)
     {  ...etc....} 
  }

The View (view name is 'fciContactUs') form-start:

@using (Html.BeginForm("Action", "Home"))  {

The view-buttons:

<input type="submit" name="TrySubmit" value="Submit" />
<input type="submit" name="DoClearForm" value="Clear Form" />

Further, this IsValidName method ALWAYS returns false and the methods NEVER get executed.

I am concerned that there is an inconsistency in the action-name, the view-name, the controller-name, the button-names and the ActionNameSelectorAttribute class override.

I am new to MVC and this whole thing has got me twisted up.

Any comments or assistance will be greatly appreciated.

回答1:

Let's start with the HttpParamActionAttribute. This inherits from ActionNameSelectorAttribute which:

Represents an attribute that affects the selection of an action method.

so, when applied to an action, and a request (get or post) comes in, this attribute will be called to see if that action is indeed the correct action.

It determines this by calling IsValidName (slightly confusing method, would be better 'IsMatchingAction' or similar). IsvalidName:

Determines whether the action name is valid in the specified controller context.

With parameters:

controllerContext: The controller context
actionName: The name of the action
methodInfo: Information about the action method

taken from the blog post, you can get the request and all the values on that request via the controllerContext.

This returns true if it matches and false if it isn't the action we're looking for.

When this gets hit:

  • actionName = the action name from the BeginForm, hardcoded to "Action"
  • methodInfo = the controller-action (method) that the attribute is applied to, eg TrySubmit

So the first check:

if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
    return true;

is just a catch-all in the case where you've specified something other than Action in your BeginForm so that it goes to the requested action (ie not "Action").

The second check checks that you have specified "Action" in the BeginForm

Now the "clever" part:

var request = controllerContext.RequestContext.HttpContext.Request;
return request[methodInfo.Name] != null;

this checks all the request parameters to see if there is a parameter that matches the controller-action (method).

When you click a submit button, the name of that submit button is passed in the request parameters

So if you have:

<input type="submit" name="TrySubmit" value="Submit" />
<input type="submit" name="DoClearForm" value="Clear Form" />

if you click your "Submit", then:

HttpContext.Request["TrySubmit"] == "Submit"
HttpContext.Request["DoClearForm"] == null

and if you click your "Clear Form", then

HttpContext.Request["TrySubmit"] == null
HttpContext.Request["DoClearForm"] == "Clear Form"`

the actual value ("Submit"/"Clear Form") isn't needed, only that it's not null.

So the code

request[methodInfo.Name] 

checks the request parameters for the existance of an item which matches the current controller-action (method) name, eg:

[HttpParamAction]
public ActionResult DoClearForm() 
{

To answer your first question:

I see a compare of actionName with methodInfo.Name. How can these EVER be equal when the whole purpose is to name the method different from the controller's action.

The first compare is an override for when the BeginForm doesn't specify Action and the BeginForm's action does match the controll-action (method) name.

So, in your scenario, no they won't be equal and shouldn't be - it's the last check that's relevant.


Now the question is:

why doesn't this work for you?

The problem is that your action names don't match the expected action names, because you have this:

[ActionName("Action")]

so the HttpParamAction attribute finds your controller-action (method) and says "use this one", but then MVC says, but I'm looking for "Action" and that's not "Action" so I'll give you "The resource cannot be found". I'm not 100% of the reason it's doing this, but it's the same if you use MVC5 Route("Action") - it can't find the action having already matched it using the attribute.

If you remove [ActionName("Action")] then all should be ok.


Alternative: if you remove the first two checks (if form action = controller action and if form action != "Action") and only apply the attribute to the methods you need (and why would you apply it elsewhere?), then it seems to work fine.

Now, I'm using [Route("")] and changed:

the form to:

using (Html.BeginForm("", "", FormMethod.Post, ...

the controller to:

[HttpParamAction]
[HttpPost]
[Route("")]
public ActionResult DoClearForm() 
{

(the initial [HttpGet] also has [Route("")])

and the attribute to:

public class HttpParamActionAttribute : ActionNameSelectorAttribute 
{
    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo) 
    {
        var request = controllerContext.RequestContext.HttpContext.Request;
        return request[methodInfo.Name] != null;
    }
}

As an alternative, it looks like your "ClearForm" button simply redirects to the page. You could do this more easily with a simple @Html.ActionLink("Clear Form", "fciContractUS") and a bit of css to make it look like a button.

You'll also have an issue if you have any client-side validation (eg required fields) as you won't be able to "submit" to clear the form until they have values.