What is the right way for creating conditional forms in ASP.Net MVC3?
Overview of the problem:
I want to have some conditional logic in my form. The user makes a choice with a radio button and depending on the selection I want to hide some fields and show some other fields. This should also affect validation (hidden fields shouldn't be validated). I think this is a typical problem, but I couldn't find any examples and my own solution seems to have a lot of plumbing.
My actual problem:
Let's start with the viewmodel classes (somewhat simplified for the needs of this question):
public class Scenario
{
public Request Request { get; set; }
public Response Response { get; set; }
// … some other properties
}
public class Request
{
//some properties
}
public class Response
{
[Required]
public string ResponseType { get; set; }
[Required]
public string State { get; set; }
[Required]
[NotZero] //this is my custom validation attribute
public string ErrorCode { get; set; }
public string ErrorDescr { get; set; }
}
In my create/edit view for the Scenario
model I have a rather big form consisting of 3 tabs. On the 3rd tab I display a partial view based on the Response
model. This is where I want the conditional logic. ResponseType
property is the radio button on the form. It can have two values: NORMAL and ERROR. In the case of ERROR I want to display and validate ErrorCode
and ErrorDescr
properties. In the case of NORMAL I want to display and validate only the State
property.
My solution:
- In the Response partial view I have some jquery .hide() and .show() calls hooked up to hide/show relevant input elements.
- I modified jqu开发者_如何学JAVAery unobtrusive validation script to stop it from validating hidden fields ( http://blog.waynebrantley.com/2011/01/mvc3-validates-hidden-fields-using.html )
In the Scenario controller I have code like this:
public ActionResult Edit(int id, Scenario scenario) { Response response=scenario.Response; if (response.ResponseType != null) { if (response.ResponseType == "NORMAL") { //in this case remove validation for errorcode this.ModelState.Remove("Response.ErrorCode"); } else { //in this case remove validation for state this.ModelState.Remove("Response.State"); } } if (ModelState.IsValid) { //map to entity and save to database } }
This is a whole lot of ugly plumbing (especially the controller code - removing items from ModelState with a string key... no type safety etc), surely there must be a better way?
You might try inheriting from IValidateableObject
in your Response
class and performing the conditional validation in Validate
. Like so:
public class Response : IValidateableObject
{
[Required]
public string ResponseType { get; set; }
public string State { get; set; }
public string ErrorCode { get; set; }
public string ErrorDescr { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ResponseType == "NORMAL")
{
if (State.IsNullOrWhiteSpace)
yield return new ValidationResult("State is required", new [] { "State" });
}
// additional validations...
}
}
This is quite old now and I don't like this hanging around without an answer so I'll answer it myself.
The correct solution is to implement a custom validation attribute and also implement the IClientValidatable interface. This satisfies all the requirements - no plumbing inside the controller and consistent client side validation.
Nowadays I probably wouldn't even implement this myself - I would use an existing library like ExpressiveAnnotations which provides a nice and flexible RequiredIf attribute:
public class Response
{
[Required]
public string ResponseType { get; set; }
[RequiredIf("ResponseType == 'NORMAL'"]
public string State { get; set; }
[RequiredIf("ResponseType == 'ERROR'"]
public string ErrorCode { get; set; }
public string ErrorDescr { get; set; }
}
The attribute works both on server side and on client side once you configure it by adding a few lines to Global.asax and including a javascript with the validators. You can read the details on the project page.
精彩评论