views:

240

answers:

2

There are a lot of articles devoted to working with data in MVC, and nothing about MVC 2.

So my question is: what is the proper way to handle POST-query and validate it.

Assume we have 2 actions. Both of them operates over the same entity, but each action has its own separated set of object properties that should be bound in automatic manner. For example:

  • Action "A" should bind only "Name" property of object, taken from POST-request
  • Action "B" should bind only "Date" property of object, taken from POST-request

As far as I understand - we cannot use Bind attribute in this case.

So - what are the best practices in MVC2 to handle POST-data and probably validate it?

UPD:
After Actions performed - additional logic will be applied to the objects so they become valid and ready to store in persistent layer. For action "A" - it will be setting up Date to current date.

+3  A: 

I personally don't like using domain model classes as my view model. I find it causes problems with validation, formatting, and generally feels wrong. In fact, I'd not actually use a DateTime property on my view model at all (I'd format it as a string in my controller).

I would use two seperate view models, each with validation attributes, exposed as properties of your primary view model:

NOTE: I've left how to combining posted view-models with the main view model as an exercise for you, since there's several ways of approaching it

public class ActionAViewModel
{
    [Required(ErrorMessage="Please enter your name")]
    public string Name { get; set; }
}

public class ActionBViewModel
{
    [Required(ErrorMessage="Please enter your date")]
    // You could use a regex or custom attribute to do date validation,
    // allowing you to have a custom error message for badly formatted
    // dates
    public string Date { get; set; }
}

public class PageViewModel
{
    public ActionAViewModel ActionA { get; set; }
    public ActionBViewModel ActionB { get; set; }
}

public class PageController
{
    public ActionResult Index()
    {
        var viewModel = new PageViewModel
        {
            ActionA = new ActionAViewModel { Name = "Test" }
            ActionB = new ActionBViewModel { Date = DateTime.Today.ToString(); }
        };

        return View(viewModel);
    }

    // The [Bind] prefix is there for when you use 
    // <%= Html.TextBoxFor(x => x.ActionA.Name) %>
    public ActionResult ActionA(
        [Bind(Prefix="ActionA")] ActionAViewModel viewModel)
    {
        if (ModelState.IsValid)
        {
            // Load model, update the Name, and commit the change
        }
        else
        {
            // Display Index with viewModel
            // and default ActionBViewModel
        }
    }

    public ActionResult ActionB(
        [Bind(Prefix="ActionB")] ActionBViewModel viewModel)
    {
        if (ModelState.IsValid)
        {
            // Load model, update the Date, and commit the change
        }
        else
        {
            // Display Index with viewModel
            // and default ActionAViewModel
        }
    }
}
Richard Szalay
I need some time to get what this code does. Thanks for answer ;-)
zerkms
so - you're still using validation specified as attributes at viewmodels in real life? i would like to move validation logic to service application level, to separate business logic. in this case i don't see any possibility of doing validation except of custom if(...) elseif(...) etc (incapsulated in some Validation class)
zerkms
As you've probably discovered, the validation system is completely plugable so you don't need to use attributes. The main thing is separating view models to get the most out of the model binding.
Richard Szalay
Also, don't confuse *input* validation with *domain validation*. After loading the model and updating the date, you could run your own custom validation logic against the model to check, for example, for invalid date ranges.
Richard Szalay
"the validation system is completely plugable so you don't need to use attributes." -- how then?
zerkms
"Also, don't confuse input validation with domain validation." -- if you will separate 2 this kinds of validations - then you can get the situation, when input validation return false (field not filled) thus domain validation doesn't triggered (if it were - we could get false, because of date out of specific range). So now user have to submit form twice, which is odd.
zerkms
You can implement a custom validation provider by subclassing `ModelValidatorProvider`. See here for an example: http://weblogs.asp.net/srkirkland/archive/2010/03/02/nhibernate-validator-asp-net-mvc-2-model-validation.aspx
Richard Szalay
Also, you can still choose to run limited validation even if the input validation marks it as invalid.
Richard Szalay
+2  A: 

One possible way to handle POST data and add validation, is with a custom model binder. Here is a small sample of what i used recently to add custom validation to POST-form data :

public class Customer
{
    public string Name { get; set; }
    public DateTime Date { get; set; }
}


public class PageController : Controller
{
    [HttpPost]
    public ActionResult ActionA(Customer customer)
    {
        if(ModelState.IsValid) {
        //do something with the customer
        }
    }

    [HttpPost]
    public ActionResult ActionB(Customer customer)
    {
       if(ModelState.IsValid) { 
       //do something with the customer
       }
    }
}

A CustomerModelBinder will be something like that:

    public class CustomerModelBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
        if (propertyDescriptor.Name == "Name") //or date or whatever else you want
        {


            //Access your Name property with valueprovider and do some magic before you bind it to the model.
            //To add validation errors do (simple stuff)
            if(string.IsNullOrEmpty(bindingContext.ValueProvider.GetValue("Name").AttemptedValue))
                bindingContext.ModelState.AddModelError("Name", "Please enter a valid name");

            //Any complex validation
        }
        else
        {
            //call the usual binder otherwise. I noticed that in this way you can use DataAnnotations as well.
            base.BindProperty(controllerContext, bindingContext, propertyDescriptor); 
        }
    }

and in the global.asax put

ModelBinders.Binders.Add(typeof(Customer), new CustomerModelBinder());

If you want not to bind Name property (just Date) when you call ActionB, then just make one more custom Model Binder and in the "if" statement, put to return the null, or the already existing value, or whatever you want. Then in the controller put:

[HttpPost]
public ActionResult([ModelBinder(typeof(CustomerAModelBinder))] Customer customer)

[HttpPost]
public ActionResult([ModelBinder(typeof(CustomerBModelBinder))] Customer customer)

Where customerAmodelbinder will bind only name and customerBmodelbinder will bind only date.

This is the easiest way i have found, to validate model binding, and i have achieved some very cool results with complex view models. I bet there is something out there that i have missed, and maybe a more expert can answer. Hope i got your question right...:)

goldenelf2
yep, you got the question right, but separation of validation to two parts: input validation (when we compose ViewModel, Customer in your sample) and business logic validation (when we check if the data is meets application mandatories) looks odd isn't it?
zerkms
i added something more to the solution. Well it is odd. The ideal way in my opinion was to validate in one spot, most preferably before the persistence to a db, file etc. I tried DataAnnotations, but i figured that i couldnt add custom validation messages, when i entered a wrong date. So i had to seperate the validation logic in two parts. I think this is the way of aps.net MVC 2 works....
goldenelf2