MVC – Validating Additional Required Inputs

Posted September 26th, 2010 in Uncategorized by cody

In MVC, common validation can be done with a trivial amount of code using attributes on your model views, but once you step outside of the simple required, length, and type validations things can get complicated in a hurry.

Our client needed a view that required the user to provide additonal input if a question was answered as yes.  Providing this functionality required more than a simple [Required] attribute on the display model.

There are multiple ways this can be done; this post will provide a quick tutorial on how we went about implementing a solution.

We had already chosen to use the JQuery validation library provided by the MVC2 template.  The function __MVC_EnableClientValidation(validationContext) in the MicrosoftMvcJQueryValidation.js file must be modified to add hidden elements to the “ignore” list:

   1: var options = {
   2:         errorClass: "input-validation-error",
   3:         //validClass: "",
   4:         errorElement: "span",
   5:         errorPlacement: function (error, element) {
   6:             var messageSpan = fieldToMessageMappings[element.attr("name")];
   7:             $(messageSpan).empty();
   8:             $(messageSpan).removeClass("field-validation-valid");
   9:             $(messageSpan).addClass("field-validation-error");
  10:             //error.removeClass("input-validation-error");
  11:             error.addClass("field-validation-error");
  12:             error.attr("_for_validation_message", messageSpan);
  13:             error.appendTo(messageSpan);
  14:         },
  15:         messages: errorMessagesObj,
  16:         rules: rulesObj,
  17:         invalidHandler: function () { theForm.trigger('invalidHandler'); },
  18:         ignore: ":not(:visible)",
  19:         success: function (label) {
  20:             var messageSpan = $(label.attr("_for_validation_message"));
  21:             $(messageSpan).empty();
  22:             $(messageSpan).addClass("field-validation-valid");
  23:             $(messageSpan).removeClass("field-validation-error");
  24:         }
  25:     };

The next step is to create your question/additional input model:

   1: [Required()]
   2: [Display(Name = "Do you like MVC2 better than WebForms?")]
   3: [YesNoControl(new string[2] { "Date", "Why" }, "DoesLikeMvcBetter")]
   4: public bool DoesLikeMvcBetter{ get; set; }
   5: [Required(ErrorMessage = "Date is required.")]
   6: public DateTime Date { get; set; }
   7: [Required(ErrorMessage = "A reason is required.")]
   8: public string Why { get; set; }

The YesNoControl attribute will be discussed later in this post.  The model needs at least two properties, the question control as a boolean and at least one other control that will be validated, but can display any additional number of controls.  In this example, there are two additional controls, one for the date the questions is answered and a reason.

   1: <div>
   2:     <%= Html.LabelFor(m => m.IsMVCBetterThanWebForms)%>
   3:     <span style="display:block;">
   4:         <%= Html.RadioButton("IsMVCBetterThanWebForms", "true", Model != null ? Model.IsMVCBetterThanWebForms : false, new { @onclick = "$('#IsMVCBetterThanWebForms_span').show()" })%><label for="Yes" style="width:20px;display:inline">Yes</label><br />
   5:         <span id="IsMVCBetterThanWebForms_span" style="display: <%= Model != null && Model.IsMVCBetterThanWebForms ? "normal" : "none" %>; width:800px; position:relative; left:25px">
   6:             Date:<%= Html.EditorFor(m => m.Date) %><%= Html.ValidationMessageFor(m => m.Date)%>
   7:             and Why: <%= Html.EditorFor(m => m.Why) %><%= Html.ValidationMessageFor(m => m.Why)%>
   8:         </span>
   9:     </span>
  10:     <%= Html.RadioButton("IsMVCBetterThanWebForms", "false", Model != null ? !Model.IsMVCBetterThanWebForms : false, new { @onclick = "$('#IsMVCBetterThanWebForms_span').hide()" })%><label for="No" style="width:20px;display:inline-block;">No</label><br />
  11:     <%= Html.ValidationMessageFor(m => m.IsMVCBetterThanWebForms)%>
  12: </div>

Then in your view you add markup that displays a radio button for the yes/no answer.  This markup adds the radio button that the user must select yes or no.  If the user selects yes, then the follow-up questions are display and require input before the form will post successfully.

So now we have a question control.

image

If the user doesn’t enter a value, the error is caught client-side and the form will not post.

image

If the user selects “Yes”, additional inputs are displayed.

image

And must be answered before proceeding as well.

image

This takes care of the client side validation, but since the server validates the model during binding, ModelState.IsValid will return false if the user answered “No” since Date and Why are empty.  This could be handled manually in the controller code, but the route we chose was to let  model binding handle it.

The next step is to create an attribute to use to decorate the model view:

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
   2:    public sealed class YesNoControlAttribute : Attribute
   3:    {
   4:        private static string defaultTemplateName;
   5:        public static string DefaultTemplateName
   6:        {
   7:            get
   8:            {
   9:                if (string.IsNullOrEmpty(defaultTemplateName))
  10:                {
  11:                    defaultTemplateName = "YesNoControl";
  12:                }
  13:
  14:                return defaultTemplateName;
  15:            }
  16:            set
  17:            {
  18:                defaultTemplateName = value;
  19:            }
  20:        }
  21:        public string TemplateName { get; private set; }
  22:        public string[] HiddenControls { get; private set; }
  23:        public IDictionary<string, object> HtmlAttributes { get; private set; }
  24:        public string ControlName { get; set; }
  25:
  26:        public YesNoControlAttribute(object hiddenControls, string controlName)
  27:            : this(DefaultTemplateName, hiddenControls, controlName, null)
  28:        {
  29:        }
  30:
  31:        public YesNoControlAttribute(object hiddenControls, string controlName, object htmlAttributes)
  32:            : this(DefaultTemplateName, hiddenControls, controlName, htmlAttributes)
  33:        {
  34:        }
  35:
  36:        public YesNoControlAttribute(string templateName, object hiddenControls, string controlName, object htmlAttributes)
  37:        {
  38:            if (string.IsNullOrEmpty(templateName))
  39:            {
  40:                throw new ArgumentException("Template name cannot be empty.");
  41:            }
  42:
  43:            if (string.IsNullOrEmpty(controlName))
  44:            {
  45:                throw new ArgumentException("Control field cannot be empty.");
  46:            }
  47:
  48:            if (((string[])hiddenControls).Count() < 1)
  49:            {
  50:                throw new ArgumentException("Hidden Controls must contain at least one control name.");
  51:            }
  52:
  53:            TemplateName = templateName;
  54:            ControlName = controlName;
  55:            HiddenControls = (string[])hiddenControls;
  56:            HtmlAttributes = new RouteValueDictionary(htmlAttributes);
  57:        }
  58:
  59:    }

This class inherits from the attribute and allows you to decorate a property and add the name of the controls that will be displayed and validated as a required field if the use answers as yes.

Then create a custom model binder to handle the binding of your question model.  If you are not familiar with custom model binding there are many sources that have covered that topic.

   1: public class QuestionDataModelBinder : IModelBinder
   2:     {
   3:         public new BindResult BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:         {
   5:             var b = new BindResult(base.BindModel(controllerContext, bindingContext), null);
   6:             var pdm = b.Value as AllMyQuestionsModel;
   7:
   8:             if(pdm == null) return b;
   9:
  10:             var props = pdm.GetType().GetProperties();
  11:
  12:             // this checks if any of the properties have a custom attribute of "YesNoControlAttribute"
  13:             // if it does, it checks to see if it's false, and removes the errors of the controls specified
  14:             foreach (string ctlName in (from propertyInfo in props
  15:                                         let isYes = propertyInfo.GetCustomAttributes(typeof (YesNoControlAttribute), false).Any()
  16:                                         where isYes
  17:                                         let res = bindingContext.ValueProvider.GetValue(propertyInfo.Name)
  18:                                         where res != null && bool.FalseString.Equals(res.AttemptedValue, StringComparison.CurrentCultureIgnoreCase)
  19:                                         select propertyInfo).SelectMany(propertyInfo => Enumerable.SelectMany<string[], string>((from attrib in propertyInfo.GetCustomAttributes(typeof (YesNoControlAttribute), false)
  20:                                                                                                                                  select ((YesNoControlAttribute) attrib).HiddenControls), ctls => ctls)))
  21:             {
  22:                 RemoveErrors(bindingContext, ctlName);
  23:             }
  24:             return b;
  25:         }
  26:     }

This class checks to see if any properties on the model it’s binding have a “YesNoControl” attribute.  If the attempted value is false, it removes the model state errors that are on any of the controls specified in the attribute.

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)

WPF WTF – Data Binding Happens When, Now?

Posted September 24th, 2010 in Uncategorized by Matt

Data binding in WPF still seems to be a bit of a black art. In a recent project, I ran into an issue where data binding was happening considerably later than expected.

Here’s the scenario: we are presenting the user with a modal dialog where they can, among other things, choose a specific font for use in a specific area of the application from a restricted list of specified fonts. The modal dialog shouldn’t be concerned with retrieving the font list because, in our particular MVP Pattern, that’s not its job.

We’re storing the font information in a very simple class.

[Serializable] public class FontInformation { public string FontName { get; set; } public int FontSize { get; set; } }

“But Matt! FontFamily is available in System.Drawing….” Yes, Carl, I know. (Apologies if your name is not Carl.) We’re planning on serializing this information for later storage in user preferences, and we’d like to avoid building in unnecessary dependencies where possible.

Here’s our first stab at the properties in the modal dialog, and how it’s implemented in the XAML.

protected internal IEnumerable<FontInformation> AvailableFonts { get { return lstFont.DataContext as IEnumerable<FontInformation>; } set { lstFont.DataContext = value; } } public FontInformation SelectedFont { get { return lstFont.SelectedItem as FontInformation; } set { lstFont.SelectedItem = value; } }

<ComboBox Name="lstFont" ItemsSource="{Binding}"> <ComboBox.ItemTemplate> <DataTemplate> <!--display the font name, written in the selected font, size, and style.--> <TextBlock Text="{Binding Path=FontName}" FontFamily="{Binding Path=FontName}" FontSize="{Binding Path=FontSize}"/> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox>

The combo box draws its list of available fonts from the property setter and displays each font drawn in its own…uh…font. This implementation allows the calling code to be completely agnostic of how the View is displaying allowed fonts and setting the selected font. Here’s where the problem reared its head, in the calling code that readies the modal dialog.

MessageOptionsForm msgOptionsForm = new MessageOptionsForm(); msgOptionsForm.AvailableFonts = this.OptionsController.AvailableMessageFonts; msgOptionsForm.SelectedFont = this.FlipbookOptions.MessageFontInformation;

I naively thought that, after setting DataContext on the combo box, that the child items would be available for selection. However, when we try to set the SelectedFont property before the control loads, data binding has not occurred yet, and the setter for SelectedFont does a whole bunch of nothing. (Hey, at least it doesn’t blow up.)

Now you have two options.

  1. Track the SelectedFont with a private FontInformation instance and set it when the window loads. This creates a point of confusion for the programmer who follows me since setting the value of that private instance will do, again, a whole bunch of nothing.
  2. Get crafty.

Here’s the modified implementation.

public FontInformation SelectedFont { get { return lstFont.SelectedItem as FontInformation; } set { if (lstFont.IsLoaded) { lstFont.SelectedItem = value; } else { //setter might get triggered before options have finished loading. lstFont.Loaded += delegate { lstFont.SelectedItem = value; }; } } }

The IsLoaded property tells us whether a framework element has been loaded for presentation (and thus is available for data binding). Here we are attaching an anonymous delegate to the Loaded event to select the correct option.

This implementation solves another potential problem with the first stab: if the calling code sets SelectedFont before setting AvailableFonts, nothing will happen. This is bad, and it violates the general principle that you should allow properties to be set in any order.

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)