Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Windows Azure, PHP, ...

About the author

Maarten Balliauw is currently employed as a Technical Evangelist at JetBrains. His interests are mainly web applications developed in ASP.NET (C#) or PHP and the Windows Azure cloud platform.
More about me More about me
Send mail E-mail me


ASP.NET MVC Quickly Pro NuGet Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
Maarten Balliauw - MVP - Most Valuable Professional
Maarten Balliauw - ASPInsider

Search

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright Maarten Balliauw 2013


Supporting multiple submit buttons on an ASP.NET MVC view

Multiple buttons on an ASP.NET MVC view A while ago, I was asked for advice on how to support multiple submit buttons in an ASP.NET MVC application, preferably without using any JavaScript. The idea was that a form could contain more than one submit button issuing a form post to a different controller action.

The above situation can be solved in many ways, one a bit cleaner than the other. For example, one could post the form back to one action method and determine which method should be called from that action method. Good solution, however: not standardized within a project and just not that maintainable… A better solution in this case was to create an ActionNameSelectorAttribute.

Whenever you decorate an action method in a controller with the ActionNameSelectorAttribute (or a subclass), ASP.NET MVC will use this attribute to determine which action method to call. For example, one of the ASP.NET MVC ActionNameSelectorAttribute subclasses is the ActionNameAttribute. Guess what the action name for the following code snippet will be for ASP.NET MVC:

public class HomeController : Controller
{
    [ActionName("Index")]
    public ActionResult Abcdefghij()
    {
        return View();
    }
}

That’s correct: this action method will be called Index instead of Abcdefghij. What happens at runtime is that ASP.NET MVC checks the ActionNameAttribute and asks if it applies for a specific request. Now let’s see if we can use this behavior for our multiple submit button scenario.

kick it on DotNetKicks.com

The view

Since our view should not be aware of the server-side plumbing, we can simply create a view that looks like this.

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcMultiButton.Models.Person>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "//www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Create person</title>
    <script src="<%=Url.Content("~/Scripts/MicrosoftAjax.js")%>" type="text/javascript"></script>
    <script src="<%=Url.Content("~/Scripts/MicrosoftMvcAjax.js")%>" type="text/javascript"></script>
</head>
<body>

    <% Html.EnableClientValidation(); %>
    <% using (Html.BeginForm()) {%>

        <fieldset>
            <legend>Create person</legend>
            <p>
                <%= Html.LabelFor(model => model.Name) %>
                <%= Html.TextBoxFor(model => model.Name) %>
                <%= Html.ValidationMessageFor(model => model.Name) %>
            </p>
            <p>
                <%= Html.LabelFor(model => model.Email) %>
                <%= Html.TextBoxFor(model => model.Email) %>
                <%= Html.ValidationMessageFor(model => model.Email) %>
            </p>
            <p>
                <input type="submit" value="Cancel" name="action" />
                <input type="submit" value="Create" name="action" />
            </p>
        </fieldset>

    <% } %>

    <div>
        <%=Html.ActionLink("Back to List", "Index") %>
    </div>

</body>
</html>

Note the two submit buttons (namely “Cancel” and “Create”), both named “action” but with a different value attribute.

The controller

Our controller should also not contain too much logic for determining the correct action method to be called. Here’s what I propose:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new Person());
    }

    [HttpPost]
    [MultiButton(MatchFormKey="action", MatchFormValue="Cancel")]
    public ActionResult Cancel()
    {
        return Content("Cancel clicked");
    }

    [HttpPost]
    [MultiButton(MatchFormKey = "action", MatchFormValue = "Create")]
    public ActionResult Create(Person person)
    {
        return Content("Create clicked");
    }
}

Some things to note:

  • There’s the Index action method which just renders the view described previously.
  • There’s a Cancel action method which will trigger when clicking the Cancel button.
  • There’s a Create action method which will trigger when clicking the Create button.

Now how do these last two work… You may also have noticed the MultiButtonAttribute being applied. We’ll see the implementation in a minute. In short, this is a subclass for the ActionNameSelectorAttribute, triggering on the parameters MatchFormKey and MatchFormValues. Now let’s see how the MultiButtonAttribute class is built…

The MultiButtonAttribute class

Now do be surprised of the amount of code that is coming…

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MultiButtonAttribute : ActionNameSelectorAttribute
{
    public string MatchFormKey { get; set; }
    public string MatchFormValue { get; set; }

    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
    {
        return controllerContext.HttpContext.Request[MatchFormKey] != null &&
            controllerContext.HttpContext.Request[MatchFormKey] == MatchFormValue;
    }
}

When applying the MultiButtonAttribute to an action method, ASP.NET MVC will come and call the IsValidName method. Next, we just check if the MatchFormKey value is one of the request keys, and the MatchFormValue matches the value in the request. Simple, straightforward and re-usable.

kick it on DotNetKicks.com


Comments (12) -

Christian Weiß Austria |

Thursday, November 26, 2009 6:34 PM

Christian Wei&#223;

hi maarten!
that's a really nice approach. I've also tried to come up with a good solution for this problem, but my ideas were far more complex Smile

thanks for this post!

regards!

Miguel United States |

Thursday, November 26, 2009 8:00 PM

Miguel

Hi Maarten. This is very elegant, bravo! I did something similar where I had a variable number of buttons that could submit a form, I ended up delegating the button html generation in the form to an HtmlHelper in the view and in the controller I had a method parse out the form value to return the correct action which I had defined elsewhere using an enumeration. I think my method works better for what I needed to do since the button pressed was just information that went to my database but when you can justify a button altering the execution path of your controller contingent on the form submission this should come in handy. Thanks.

Thomas Eyde Norway |

Thursday, November 26, 2009 8:36 PM

Thomas Eyde

Nice approach! I learned something this evening.

However, your approach has one caveat: The action selector now depends on the button value, which, I think, is strictly a view concern. When the whimsy product owner changes his mind, he break our code. And so does localization.

I need this functionality for my list where I can delete an individual row. What I'd like is to have a button with the row-id embedded in the name:

<input type="submit" value="Delete" name="deleteRow:99" />

And have this routed to:

[HttpPost]
[MultiButton(Key = "deleteRow")]
[BindMultiButtonTo("id")]
public ActionResult DeleteRow(int id, Whatever form) {}

Maybe it's possible to combine those with some crazy inheritance chain:

[HttpPost]
[MultiButton(Key = "deleteRow", BindTo = "id")]
public ActionResult DeleteRow(int id, Whatever form) {}

That would be cool!

Thomas Eyde Norway |

Thursday, November 26, 2009 11:04 PM

Thomas Eyde

Inspired by your approach, I decided to take it a little further to satisfy my needs. And I have to tell you, this is way more elegant than my initial approach.

Now I can write actions in the following manner:

        [HttpPost]
        [MultiButton(Name = "delete", Argument = "id")]
        public ActionResult Delete(string id)
        {
            var response = System.Web.HttpContext.Current.Response;
            response.Write("Delete action was invoked with " + id);
            return View();
        }

The button can be any of:

        <input type="submit" value="not important" name="delete" />
        <input type="submit" value="not important" name="delete:id" />

And your modified attribute is as follows:

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class MultiButtonAttribute : ActionNameSelectorAttribute
    {
        public string Name { get; set; }
        public string Argument { get; set; }

        public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
        {
            var key = ButtonKeyFrom(controllerContext);
            var keyIsValid = IsValid(key);

            if (keyIsValid)
            {
                UpdateValueProviderIn(controllerContext, ValueFrom(key));
            }

            return keyIsValid;
        }

        private string ButtonKeyFrom(ControllerContext controllerContext)
        {
            var keys = controllerContext.HttpContext.Request.Params.AllKeys;
            return keys.FirstOrDefault(KeyStartsWithButtonName);
        }

        private static bool IsValid(string key)
        {
            return key != null;
        }

        private static string ValueFrom(string key)
        {
            var parts = key.Split(":".ToCharArray());
            return parts.Length < 2 ? null : parts[1];
        }

        private void UpdateValueProviderIn(ControllerContext controllerContext, string value)
        {
            if (string.IsNullOrEmpty(Argument)) return;
            controllerContext.Controller.ValueProvider[Argument] = new ValueProviderResult(value, value, null);
        }

        private bool KeyStartsWithButtonName(string key)
        {
            return key.StartsWith(Name, StringComparison.InvariantCultureIgnoreCase);
        }
    }

maartenba Belgium |

Friday, November 27, 2009 7:50 AM

maartenba

Good one and still very elegant, looks good!

dario-g Poland |

Friday, November 27, 2009 12:28 AM

dario-g

What about multi-language application. Value of submit button will be different but action should be the same.

In my solution only checks name of button.
(In polish) dario-g.com/...ionAttribute-w-ASPNET-MVC-0-92.aspx

maartenba Belgium |

Friday, November 27, 2009 7:49 AM

maartenba

You could simply add support for localized buttons in the MultiButtonAttribute and take care of translated values in there.

Peter Mounce United Kingdom |

Friday, November 27, 2009 10:41 AM

Peter Mounce

Why not make the cancel button a link to the cancel-action and style it so it looks like a button?

maartenba Belgium |

Friday, November 27, 2009 11:04 AM

maartenba

Maybe you do want to have teh data in the form and do soemthing with that prior to really canceling teh action. Or other types of buttons.

Feroze United States |

Tuesday, January 05, 2010 10:38 AM

Feroze

MultiButton required a reference. Where is ActionNameSelectorAttribute class?

maartenba Belgium |

Tuesday, January 05, 2010 10:53 AM

maartenba

In ASP.NET MVC 2

Sam United States |

Monday, January 17, 2011 1:42 AM

Sam

Great post! Works perfectly for me.

Pingbacks and trackbacks (9)+

Comments are closed