Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Azure, PHP, OpenXML, VSTS, ...

About the author

Maarten Balliauw is currently employed as .NET Technical Consultant at RealDolmen. 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 Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
View Maarten Balliauw's MVP profile

Search

Latest Twitter

    Follow me on Twitter...

    My projects

    Disclaimer

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

    © Copyright Maarten Balliauw 2010

    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

    DotNetKicks.com | Reply

    Thursday, November 26, 2009 2:51 PM

    trackback

    Supporting multiple submit buttons on an ASP.NET MVC view

    You've been kicked (a good thing) - Trackback from DotNetKicks.com

    topsy.com | Reply

    Thursday, November 26, 2009 3:09 PM

    pingback

    Pingback from topsy.com

    Twitter Trackbacks for
            
            Supporting multiple submit buttons on an ASP.NET MVC view
            [maartenballiauw.be]
            on Topsy.com

    progg.ru | Reply

    Thursday, November 26, 2009 3:41 PM

    trackback

    Поддержка нескольких submit-кнопок для одной формы в ASP.NET MVC

    Thank you for submitting this cool story - Trackback from progg.ru

    uberVU - social comments | Reply

    Thursday, November 26, 2009 3:43 PM

    trackback

    Social comments and analytics for this post

    This post was mentioned on Twitter by maartenballiauw: Blogged: Supporting multiple submit buttons on an ASP.NET MVC view - http://bit.ly/4rYiLE #aspnetmvc #aspnet #mvc

    Christian Weiß Austria | Reply

    Thursday, November 26, 2009 6:34 PM

    Christian Weiß

    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 | Reply

    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 | Reply

    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 | Reply

    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 | Reply

    Friday, November 27, 2009 7:50 AM

    maartenba

    Good one and still very elegant, looks good!

    dario-g Poland | Reply

    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 | Reply

    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.

    blog.cwa.me.uk | Reply

    Friday, November 27, 2009 9:23 AM

    pingback

    Pingback from blog.cwa.me.uk

    Reflective Perspective - Chris Alcock  » The Morning Brew #486

    Peter Mounce United Kingdom | Reply

    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 | Reply

    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 | Reply

    Tuesday, January 05, 2010 10:38 AM

    Feroze

    MultiButton required a reference. Where is ActionNameSelectorAttribute class?

    maartenba Belgium | Reply

    Tuesday, January 05, 2010 10:53 AM

    maartenba

    In ASP.NET MVC 2

    Add comment




      Country flag

    biuquote
    • Comment
    • Preview
    Loading