:::: MENU ::::

Thursday, April 12, 2012

Note: I am uses this method over the previous post as I think it is cleaner.


Firstly, you can disable client validation on your cancel button simply by adding the CSS class 'cancel' to it. See: Disable client-side validation in MVC 3 "cancel" submit button

Secondly, as well as switching on the submit element's form name as described above, you can use a custom action selector. Here's mine, which I originally took from the blog post shown in the comment:

 /// <summary>
/// Used to vary an action method based on which button in a form was pressed. This
/// is useful but is an anti-pattern because it couples the controller to names
/// used in the form elements.
/// </summary>
/// <remarks>
/// See the example at http://weblogs.asp.net/dfindley/archive/2009/05/31/asp-net-mvc-multiple-buttons-in-the-same-form.aspx
/// </remarks>
public class AcceptButtonAttribute : ActionMethodSelectorAttribute
{
   
public string ButtonName { get; set; }

   
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
   
{
       
var req = controllerContext.RequestContext.HttpContext.Request;
       
return !string.IsNullOrEmpty(req.Form[this.ButtonName]);
   
}
}

In your controller:

     [HttpPost]
   
[ActionName("Edit")]
   
[AcceptButton(ButtonName = "Cancel")]
   
public ActionResult Edit_Cancel(MyModel model)
   
{
       
return RedirectToAction("Index");
   
}

   
[HttpPost]
   
[AcceptButton(ButtonName = "Save")]
   
public ActionResult Edit(MyModel model)
   
{
       
// do real work here
   
}

Note that you need the [ActionName("Edit")] attribute to tell MVC that although using a different method name, it is for the Edit action.

And in your View:

     <input type="submit" name="Save" value="Save" />
   
<input type="submit" name="Cancel" value="Cancel" class="cancel" />
 
 More

Introduction

Recently, I started working on a large eCommerce website, implemented in ASP.NET MVC 3. I soon found that many of the forms in the site contained code similar to Figure 1, which I took an instant dislike to. The reason I dislike it is that "Action" doesn't relate to anything (i.e. it isn't an actual action name) and causes the URL to change when the form is submitted, so a URL like "/Blog/AddPost" will suddenly become "/Blog/Action". Another side-effect is that the destination action method must specify explicitly the view name. I then discovered that it was to enable multiple submit buttons by using an implementation of the ActionNameSelectorAttribute class (Figure 2).

1
2
3
4
5
@using(Html.BeginForm("Action"))
{
    <button name="Save">Save</button>
    <button name="Publish">Publish</button>
}
Figure 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SubmitButtonNameActionNameSelector
    : 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;
    }
}
Figure 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BlogController
    : Controller
{
    [SubmitButtonNameActionNameSelector]
    public ActionResult Save(...)
    {
        ...
    }
    [SubmitButtonNameActionNameSelector]
    public ActionResult Publish(...)
    {
        ...
    }
}
Figure 3

Previous technique

In other projects I've used a popular technique of binding the button's name to a parameter name in the action method and then determining the button's value, in order to branch to a different method. There are downsides to this approach but the main thing I dislike about it is that it's quite messy. The ActionNameSelectorAttribute implementation allows for a cleaner separation of behaviour for each button.

Solving the problem

To circumvent the problems I have mentioned, I decided to use a variation of the SubmitButtonNameActionNameSelectorimplementation (Figure 4).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FormActionAttribute
    : ActionNameSelectorAttribute
{
    public override bool IsValidName(
        ControllerContext controllerContext,
        string actionName,
        MethodInfo methodInfo)
    {
        return controllerContext.HttpContext.Request[Prefix + methodInfo.Name] != null
            && !controllerContext.IsChildAction;
    }
 
    public string Prefix = "Action::";
}
Figure 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BlogController
    : Controller
{
    [FormAction]
    public ActionResult Save(...)
    {
        ...
    }
    [FormAction(Prefix = "Action_")]
    public ActionResult Publish(...)
    {
        ...
    }
}
Figure 5
1
2
3
4
5
@using(Html.BeginForm())
{
    <button name="Action::Save">Save</button>
    <button name="Action_Publish">Publish</button>
}
Figure 6

Conclusion

By prefixing the button's name there is no need to have "Action" as the form's action (Figure 6), which means that the URL stays the same and there is no need to explicitly specify the view name.

More