Home > MonoRail > SubmittedWith Parameter Binder – Which button was clicked?

SubmittedWith Parameter Binder – Which button was clicked?

February 23, 2009 Neal Leave a comment Go to comments

What do you do when you want to have more than one button submit a form and vary the actions taken depending on the button press?

Previously I would have done something like:


bool resetFilter = QueryContainsKey( "ShowAll" );

where QueryContainsKey is a simple method that scans the query collection looking for the “ShowAll” key (when an html form is submitted, the name of the button is passed in the request, in the same way that other form elements are – just without a value).

This approach works reasonably well – except it’s kind of ugly, repetative (I need to apply the same logic in at least 3 controller methods) and makes testing painful as I have to remember to add a value to the Query collection.

Then I discovered the IParameterBinder interface (have a look at the DataBindAttribute code and Ken Egozi’s excellent tutorial) and built the following:

    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class SubmittedWithAttribute : Attribute, IParameterBinder
    {
        private readonly string _buttonName;

        public SubmittedWithAttribute(string buttonName)
        {
            _buttonName = buttonName;
        }

        public int CalculateParamPoints(IEngineContext context, IController controller, IControllerContext controllerContext, ParameterInfo parameterInfo)
        {
            if( parameterInfo.ParameterType.Equals( typeof(bool) ))
            {
                return 10;
            }

            return 0;
        }

        public object Bind(IEngineContext context, IController controller, IControllerContext controllerContext, ParameterInfo parameterInfo)
        {
            var collectionKeys = new List();

            if( context.Request.HttpMethod == "GET" )
            {
                collectionKeys.AddRange( context.Request.QueryString.AllKeys );
            }

            if( context.Request.HttpMethod == "POST" )
            {
                collectionKeys.AddRange(context.Request.Form.AllKeys);
            }

            // Keys must be compared using case insensitive comparer - html is not case sensitive and neither should this be
            collectionKeys.Sort(StringComparer.InvariantCultureIgnoreCase);
            return collectionKeys.BinarySearch( _buttonName, StringComparer.InvariantCultureIgnoreCase ) >= 0;
        }
    }
[/sourceode]

This allows you to define a controller method like:

[sourcecode language='csharp']
public void ViewResults(Guid questionnaireId, [DataBind("SurveyFilter")]SurveyFilterBuilder surveyFilterBuilder, [SubmittedWith( "ShowAll" )]bool resetFilter)
{
...
}

and test it just like any other method, but when the action is called from a form submit, resetFilter will indicate if the form was submitted using the ShowAll button.  The html form looks like:

$Form.FormTag("%{method='Get'}")
<fieldset>
...
<button type="submit" id="apply" title="Apply the filter to the report">Apply</button>
<button type="submit" id="showall" name="ShowAll" title="Show all results">Show all</button>
</fieldset>
$Form.EndFormTag()

Wicked!  Now I have collapsed a bunch of repeated logic into a simple attribute (that is tested) that has the added benefits of making my action method signatures more obvious and made my tests much cleaner.

Updated 24 Feb: Searching the key collection for the button name is now case-insensitive

Categories: MonoRail
  1. February 24, 2009 at 11:58 pm | #1

    Nice,

    I think you can use context.Request.Params instead of context.Request.QueryString and context.Request.Form to eliminate the GET/POST check.

    • Neal
      February 25, 2009 at 6:07 pm | #2

      @morcs

      Yes, you could use the Params collection, but given that there is a Sort and Search, the smaller the collection the better.

  1. No trackbacks yet.