views:

156

answers:

1

This doesn't make any sense to me.

Look at this ViewModel:

public class SelectService
{
    [Required]
    public int? SelectedServiceId { get; set; }
}

Look at this Action:

[HttpPost]
public virtual ActionResult SelectService()
{
    TryUpdateModel(WizardContainer.SelectService, "SelectService");
    TryValidateModel(WizardContainer.SelectService, "SelectService"); // if I remove this, ModelState.IsValid will always be true


    if (ModelState.IsValid)
    {
        return RedirectToAction("OtherAction");
    }
    else
    {
        return View(WizardContainer);
    }
}

Now read this exceprt from Apress ASP.NET MVC 2 Framework by S. Sanderson:

Whenever you use model binding to populate a model object—either by receiving it as an action method parameter, or by calling UpdateModel() or TryUpdateModel() manually—then DefaultModelBinder will automatically run the validators associated with all model objects that it has updated (i.e., ones where it has set a value on at least one property). If you update a model object in any other way, its validators will not be run unless you explicitly tell the framework to run them.

So, why then is validation never occuring when I call TryUpdateModel()? To get validation to occur, I have to explicitly validate using TryValidateModel().

UPDATE

Here is a very similar action in the same controller that works as expected:

[HttpPost]
public virtual ActionResult Index(string nextButton)
{
    TryUpdateModel(WizardContainer.Index);

    if (nextButton != null && ModelState.IsValid)
    {
        return RedirectToAction("OtherAction");
    }
    else
    {
        return View(WizardContainer.Index);
    }
}

And here is it's ViewModel:

public class Index
{
    [Required]
    public DateTime? SelectedServiceTime { get; set; }
}

Update 2

I changed some things and now it behaves as expected, but I still don't understand why.

Look at the edited action:

[HttpPost]
public virtual ActionResult SelectService()
{
    TryUpdateModel(WizardContainer.SelectService);
    //TryValidateModel(WizardContainer.SelectService, "SelectService"); // not needed anymore

    if (ModelState.IsValid)
    {
        return RedirectToAction("OtherMethod");
    }
    else
    {
        return View(WizardContainer.SelectService);
    }
}

The difference now being only that the object that I am passing to TryUpdateModel() is the object I am passing to the view instead of being a property of the object that I pass to the view. What the deal yo?

A: 

I've included both the TryUpdateModel and TryValidateModel method decompilations below. The primary difference I see is that the return of TryUpdateModel is ModelState.IsValid, but I see no explicit execution of the validators in TryUpdateModel.

TryValidateModel actually gets the validators and executes each one. There must be some subtle difference between your working and non-working cases. I can't say what the root cause is, but in your working case there are already ModelState errors added to the ModelState.Errors collection when you call TryUpdateModel. (ModelState.IsValid returns false if there are one or more errors in the ModelState.Errors collection.)

TryUpdateModel:

protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel: class
{
    if (model == null)
    {
        throw new ArgumentNullException("model");
    }
    if (valueProvider == null)
    {
        throw new ArgumentNullException("valueProvider");
    }
    Predicate<string> predicate = delegate (string propertyName) {
        return BindAttribute.IsPropertyAllowed(propertyName, base.includeProperties, base.excludeProperties);
    };
    IModelBinder binder = this.Binders.GetBinder(typeof(TModel));
    ModelBindingContext context2 = new ModelBindingContext();
    context2.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(delegate {
        return base.model;
    }, typeof(TModel));
    context2.ModelName = prefix;
    context2.ModelState = this.ModelState;
    context2.PropertyFilter = predicate;
    context2.ValueProvider = valueProvider;
    ModelBindingContext bindingContext = context2;
    binder.BindModel(base.ControllerContext, bindingContext);
    return this.ModelState.IsValid;
}

TryValidateModel:

protected internal bool TryValidateModel(object model, string prefix)
{
    if (model == null)
    {
        throw new ArgumentNullException("model");
    }
    foreach (ModelValidationResult result in ModelValidator.GetModelValidator(ModelMetadataProviders.Current.GetMetadataForType(delegate {
        return model;
    }, model.GetType()), base.ControllerContext).Validate(null))
    {
        this.ModelState.AddModelError(DefaultModelBinder.CreateSubPropertyName(prefix, result.MemberName), result.Message);
    }
    return this.ModelState.IsValid;
}
Dave Swersky
I think you are asking a question. In the working method, ModelState.IsValid = true before trying to update the model. If the required value isn't supplied in the request, then it is false.
Bigglesby
Validation doesn't occur in the body of TryUpdateModel() itself, but rather in the resulting call to DefaultModelBinder.BindModel(), which calls BindProperty() where the Errors are actually registered in ModelState.
Bigglesby