views:

2192

answers:

5

I'm trying to use MVC for a new project after having been around the block with all the samples and tutorials and such. However, I'm having a hard time figuring out where certain things should take place.

As an example, I have an entity called Profile. This entity contains the normal profile type stuff along with a DateOfBirth property that is of type DateTime. On the HTML form, the date of birth field is split into 3 fields. Now, I know I can use a custom model binder to handle this, but what if the date entered is not a valid date? Should I be checking for that in the model binder? Should all my validation go in the model binder? Is it ok to have only a few things validated in the model binder and validate the rest in the controller or the model itself?

Here's the code I have now, but it just doesn't look right to me. Seems dirty or smelly.

namespace WebSite.Models
{
    public class ProfileModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            DateTime birthDate;

            var form = controllerContext.HttpContext.Request.Form;
            var state = controllerContext.Controller.ViewData.ModelState;

            var profile = new Profile();
            profile.FirstName = form["FirstName"];
            profile.LastName = form["LastName"];
            profile.Address = form["Address"];
            profile.Address2 = form["Address2"];
            profile.City = form["City"];
            profile.State = form["State"];
            profile.Zip = form["Zip"];
            profile.Phone = form["Phone"];
            profile.Email = form["Email"];
            profile.Created = DateTime.UtcNow;
            profile.IpAddress = controllerContext.HttpContext.Request.UserHostAddress;

            var dateTemp = string.Format("{0}/{1}/{2}",
                form["BirthMonth"], form["BirthDay"], form["BirthYear"]);

            if (string.IsNullOrEmpty(dateTemp))
                state.AddModelError("BirthDate", "Required");
            else if (!DateTime.TryParse(dateTemp, out birthDate))
                state.AddModelError("BirthDate", "Invalid");
            else
                profile.BirthDate = birthDate;

            return profile;
        }        
    }
}

Building on the sample code above, how would you do the validation message for a 3 part field? In the case above, I'm using a completely separate key that doesn't actually correspond to a field in the form, because I don't want an error message to appear beside all 3 fields. I only want it to appear to the right of the Year field.

A: 

The Contact Manager sample application on the http://www.asp.net/mvc site has an excellent description of separating out your validation logic into a service layer from your controller and model.

It's well work a read

datacop
Yeah I've seen the app. That does nothing for the particular question I posed. My problem is that the model contains a DateTime field. However, the form represents this as 3 textboxes. Therefore, I must do some form of validation before the model is even bound, and I'm trying to figure the best way to do so.
Chris
Have a look at this post: http://www.hanselman.com/blog/SplittingDateTimeUnitTestingASPNETMVCCustomModelBinders.aspx
Charlino
+1  A: 

Validation should be done in multiple places, according to the functionality of each place. For example, if your model binder cannot find the submitted values into a proper DateTime value, then the binder can add a model state error. If, on the other hand, your business logic requires the date to be within a certain range, this would not be appropriate to do and the model binder; it should be in the business logic layer. Controllers can potentially add validation errors as well if, for example, the edit model cannot be transformed into an entity model.

A validation framework such as xVal makes this much simpler.

Craig Stuntz
+2  A: 

I had the same exact situation the other day...below is my model binding code. Basically it binds all the DateTime? fields of a model to month/day/year fields from a form (if possible) So, yes, I do add in the validation here, since it does seem appropriate to do so.

public class DateModelBinder : DefaultModelBinder  
    {

        protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
        {

            if (propertyDescriptor.PropertyType == typeof(DateTime?))
            {
                string DateMonth = _GetDateValue(bindingContext, propertyDescriptor.Name + "Month");
                string DateDay = _GetDateValue(bindingContext, propertyDescriptor.Name + "Day");
                string DateYear = _GetDateValue(bindingContext, propertyDescriptor.Name + "Year");
                // Try to parse the date if we have at least a month, day or year
                if (!String.IsNullOrEmpty(DateMonth) || !String.IsNullOrEmpty(DateDay) || !String.IsNullOrEmpty(DateYear))
                {
                    DateTime fullDate;
                    CultureInfo enUS = new CultureInfo("en-US");
                    // If we can parse it, set the model property
                    if (DateTime.TryParse(DateMonth + "/" + DateDay + "/" + DateYear,
                                         enUS,
                                         DateTimeStyles.None, out fullDate))
                    {
                        SetProperty(controllerContext, bindingContext, propertyDescriptor, (DateTime?)fullDate);
                    }
                    // The date is invalid, so we need to add a model error
                    else
                    {
                        string ModelPropertyName = bindingContext.ModelName;
                        if(ModelPropertyName != "")
                        {
                            ModelPropertyName += ".";
                        }
                        ModelPropertyName += propertyDescriptor.Name;
                        bindingContext.ModelState.AddModelError(ModelPropertyName, "Invalid date supplied for " + propertyDescriptor.Name);
                    }
                }
                return;
            }
            base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
        }

        // Get a property from binding context
        private string _GetDateValue(ModelBindingContext bindingContext, string key)
        {
            ValueProviderResult valueResult;
            bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName + "." + key, out valueResult);
            //Didn't work? Try without the prefix if needed...  
            // && bindingContext.FallbackToEmptyPrefix == true
            if (valueResult == null)
            {
                bindingContext.ValueProvider.TryGetValue(key, out valueResult);
            }
            if (valueResult == null)
            {
                return null;
            }
            return (string)valueResult.ConvertTo(typeof(string));
        }

    }

Note: I had some problems with bindingContext.FallbackToEmptyPrefix always being false...can't find any useful info on that, but you get the idea.

Looks like a copy/past of code from a Scott Hanselman blog post: http://www.hanselman.com/blog/SplittingDateTimeUnitTestingASPNETMVCCustomModelBinders.aspx
JMP
+3  A: 

Sometimes the model is a view-model, not a domain model. In this case you could benefit from separating those two and design the view model to match your view.

Now you can let the view model validate the input and parse the three fields into a DateTime. Then it can update the domain model:

public ActionResult SomeAction(ViewModel vm)
{
    if (vm.IsValid)
    {
     var dm = repositoryOrSomething.GetDomainModel();
     vm.Update(dm);
    }

    // more code...
}
Thomas Eyde
+5  A: 

I think it is reasonable to do validation in the model binder. As Craig points out, the validation is mostly the property of your business domain, however:

  1. Sometimes your model is just a dumb presentation model, not a business object
  2. There are various mechanisms you can use to surface the validation knowledge into the model binder.

Thomas gives you an exmaple of #1.

An example of #2 is when you declaratively desribe validation knowlege using attributes (like the DataAnnotation attribute [Required]), or inject some business layer validation service into a custom model binder. In these situations the model binder is an ideal place to take care of validation.

That being said, model binding (finding, converting, and shuffling data into an object) and validation (data meets our specifications) are two seperate concerns. You could argue that they should be seperate phases/components/extensibility points, but we have what we have, although the DefaultModelBinder makes some distinction between these two responsibilities. If all you want to do is provide some validation for a specific type of object you can derive from the DefaultModelBinder and override the OnPropertyValidating method for property level validations or OnModelUpdated if you need the holistic view.

Here's the code I have now, but it just doesn't look right to me. Seems dirty or smelly.

For your specific code I would try to write a model binder for DateTime only. The default model binder can take care of binding firstname, lastname, etc., and delegate to your custom model binder when it reaches a DateTime property on the Profile. In addition, try using the valueProvider in the bindingContext instead of going directly to the form. These things can give you more flexibility.

More thoughts here: 6 Tips for ASP.NET MVC Model Binding.

OdeToCode