Using Data Annotations with similar models and the same view to have different validation - asp.net-mvc

I have two separate classes derived from the same interface, but have different validation/data annotations assigned. The requirement is that the same data needs to be collected, but on one screen nothing is required (a save screen), but on the other there are some required fields (a submit/finalize screen.)
I've made a PartialView that is to be used in two separate View, one for save, one for final submit.
I've tried using the parent Interface as the View's model, however my validators don't fire (as I expect, I'm guessing that because the Interface itself doesn't have any annotations, nothing will fire.) Is there a way to have the page dynamically choose one class or the other depending on which page I'm using instead of the Interface?
As a side-note, this is being done in ASP.net MVC 3 with Razor.

You can achieve what you want with one class, and a little lateral thinking.
First, create your class, with the validation baked in. Next, create a custom ModelValidatorProvider inheriting from DataAnnotationsModelValidatorProvider, like so:
public class MyMetadataValidatorProvider : DataAnnotationsModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
var vals = base.GetValidators(metadata, context, attributes);
// check to see if any keys have been inserted
if (context.Controller.ViewData.Keys.Count > 0)
{
// check if we have a key named "NoValidate" with a value of true
// do not return the validtors if we do
if ((bool)context.Controller.ViewData.FirstOrDefault(k => k.Key == "NoValidate").Value)
{
// we do not want to return our validators, return an empty list
return new List<ModelValidator>();
}
}
else
{
// check if the form has a key named "NoValidate" with a value of true
// do not return the validtors if we do
if (context.HttpContext.Request.Form["NoValidate"].ToLowerInvariant() == "true")
{
// we do not want to return our validators, return an empty list
return new List<ModelValidator>();
}
}
// we want to return our validators
return vals;
}
}
Next, register the custom ModelValidatorProvider in Application_Start in Global.asax.cs, like so:
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MyMetadataValidatorProvider());
Then, add the following to your view (this will govern whether the validators are returned when the form is POSTed):
#Html.Hidden("NoValidate", ViewData.FirstOrDefault(k => k.Key == "NoValidate").Value)
Finally, add actions like the following:
public ActionResult Index()
{
var model = new MyModel();
// this will set validation to appear
ViewData.Add("NoValidate", false);
// this will suppress validation
ViewData.Add("NoValidate", true);
return View(model);
}
[HttpPost]
public ActionResult Index(MyModel model)
{
// we DO want validation, so let's test for it in addition
// to testing if the ModelState is valid
if (Request.Form["NoValidate"].ToLowerInvariant() != "true" && ModelState.IsValid)
{
ModelState.Clear();
var newmodel = new MyModel();
ViewData.Add("NoValidate", true);
return View(newmodel);
}
ViewData.Add("NoValidate", false);
return View(model);
}
Note that you can control whether the validation appears in your GET action by setting the NoValidate key in ViewData as you want. On the POST, the validation is governed by the form value for NoValidate.
IMPORTANT NOTE: In your action which requires validation, you need to add a test to confirm that the Form does not have the key NoValidate, or its value is not True, in order to enforce that a user cannot avoid the validation.
UPDATE
At first, I had validation only appearing when certain conditions were true. Idecided this was a BAD IDEA, so now validation will only be suppressed if the conditions are true.

Each view should be strongly typed to a separate view model. Each viewmodel then has the validation logic on it (annotations) or inherits from a base that has the required validation on it.
Any logic that cannot be inherited is simply set on your ViewModel itself. If its a small moderl I would consider just copy/paste and two separate viewmodels with their own set of attributes.
You can use AutoMapper to easily map between some concrete object that implements your interface and your ViewModels.

Could you use one class? You can create a filter that allows you to manage the validation errors for an action. In your case you can add an attribute to the Save action and ignore the required errors, but the validations will run for the submit/finalize action. This example will discard all the errors.
public class DontValidateEmailAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
var modelState = filterContext.Controller.ViewData.ModelState;
var incomingValues = filterContext.Controller.ValueProvider;
foreach (var key in modelState.Keys)
modelState[key].Errors.Clear();
}
}
I learnt this technique from Steve Sanderson's Pro ASP NET MVC 3. He uses the technique to validate a model that has required fields but the data entry is a multistep wizard. If the value has not been returned in the form post, he removes the errors for that property.

Related

Is it possible to auto update only selected properties on an existent entity object without touching the others

Say I have a bunch of boolean properties on my entity class public bool isActive etc. Values which will be manipulated by setting check boxes in a web application. I will ONLY be posting back the one changed name/value pair and the primary key at a time, say { isActive : true , NewsPageID: 34 } and the default model binder will create a NewsPage object with only those two properties set. Now if I run the below code it will not only update the values for the properties that have been set on the NewsPage object created by the model binder but of course also attempt to null all the other non set values for the existent entity object because they are not set on NewsPage object created by the model binder.
Is it possible to somehow tell entity framework not to look at the properties that are set to null and attempt to persist those changes back to the retrieved entity object and hence database ? Perhaps there's some code I can write that will only utilize the non-null values and their property names on the NewsPage object created by model binder and only attempt to update those particular properties ?
[HttpPost]
public PartialViewResult SaveNews(NewsPage Np)
{
Np.ModifyDate = DateTime.Now;
_db.NewsPages.Attach(Np);
_db.ObjectStateManager.ChangeObjectState(Np, System.Data.EntityState.Modified);
_db.SaveChanges();
_db.Dispose();
return PartialView("MonthNewsData");
}
I can of course do something like below, but I have a feeling it's not the optimal solution. Especially considering that I have like 6 boolean properties that I need to set.
[HttpPost]
public PartialViewResult SaveNews(int NewsPageID, bool isActive, bool isOnFrontPage)
{
if (isActive != null) { //Get entity and update this property }
if (isOnFontPage != null) { //Get entity and update this property }
}
API is not strongly typed but you can do it as follows. DbContext API has better support for this.
[HttpPost]
public PartialViewResult SaveNews(NewsPage Np)
{
Np.ModifyDate = DateTime.Now;
_db.NewsPages.Attach(Np);
var entry = _db.ObjectStateManager.GetObjectStateEntry(Np);
var cv = entry.CurrentValues;
if (isActive)
{
cv.SetBoolean(cv.GetOrdinal("isActive"), true);
}
_db.SaveChanges();
_db.Dispose();
return PartialView("MonthNewsData");
}
You can go for two options
Register a custom model binder for that action. In the custom model binder you have to get the complete object from the database and only update the POSTed properties.
Use a view model. Instead of directly having the NewsPage model as the action parameter. You can create a custom view model that wraps the necessary properties. Inside the action you have to make a call to db to get the complete NewsPage instance and update only the corresponding properties from the view model.
Somewhat ugly, but did the trick in my case without having to create and register custom model binder or using multiple if statements.
[HttpPost]
public void SaveNews(string propname, bool propvalue, int PageID)
{
var prop = typeof(NewsPage).GetProperties().FirstOrDefault(x => x.Name.ToLower() == propname.ToLower());
var Np = _db.NewsPages.FirstOrDefault(x => x.PageID == PageID);
prop.SetValue(Np, propvalue, null);
Np.ModifyDate = DateTime.Now;
_db.SaveChanges();
_db.Dispose();
}

How can I return additional (i.e. more than just field=>error message) validation information from custom validation code to the Controller or View?

I am looking for a way to return the following information from my custom validation code:
public enum ValidationErrorTypeFlags
{
Error_Input = 1 << 0, // a "field specific" error which is checked both clientside and serverside
Error_Input_ServerSide = 1 << 1, // a "field specific" error which can only be checked serverside
Error_General = 1 << 2 // serverside general error
}
Inside the validation code (either an IValidatableObject or a ValidationAttribute), when I detect an error, I would like to be able to associate one of the above error types with the ValidationResult.
Then I want to be able to iterate through the validation errors in either the Controller or the View and distinguish between these error types.
I'm currently using MVC 3 (happy to upgrade to 4).
NB:
ModelState does not preserve ValidationResults AFAIK - you can only access errors in ViewData.ModelState.Values.Items[x].Errors - and these have been converted to System.Web.Mvc.ModelError
It seems that MVC validation only allows you to access [key, 'error message'] type validation results after validation has completed.
The hack I'm using at present is to decorate the error message inside the custom validation code:
var field = new[] { validationContext.DisplayName };
return new ValidationResult("+Invalid format - use yyyy-mm-dd", field);
And then look for error messages which start with +,-,* in the controller.
From custom validation code (no idea how to accomplish from built-in ones) you can do that by creating a custom ValidationResult class by inheriting from the base and return from your custom validation attributes.
public class CustomValidationResult: ValidationResult
{
// additional properties
}
Then from the controller you can cast and check if the validation result is your custom type and act accordingly.
Update:
The above idea don't work because the ValidationResult class is in DataAnnotations assembly and they are converted into ModelValidationResult and that's all we can access in MVC.
It seems passing extra information from the data annotation validations to the MVC looks like not quite easy!
I was going through the source code and found that it is the ValidatableObjectAdapter that converts the IEnumerable<ValidationResult> into IEnumerable<ModelValidationResult>. I don't see much benefit on extending this class but we can easily create a custom ValidatableObjectAdapter by implementing the ModelValidatorand duplicating the Validate code.
We have to create a custom ModelValidationResult and custom ValidationResult(it is this custom ValidationResult we will b returning from validations) and in the ConvertResults method we can put our conversion code that takes care of the additional information.
public class CustomValidatableObjectAdapter : ModelValidator
{
public CustomValidatableObjectAdapter(ModelMetadata metadata, ControllerContext context)
: base(metadata, context)
{
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
object model = Metadata.Model;
if (model == null)
{
return Enumerable.Empty<ModelValidationResult>();
}
IValidatableObject validatable = model as IValidatableObject;
if (validatable == null)
{
throw new Exception("model is of not type validatable");
}
ValidationContext validationContext = new ValidationContext(validatable, null, null);
return ConvertResults(validatable.Validate(validationContext));
}
private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> results)
{
foreach (ValidationResult result in results)
{
// iterate the ValidationResult enumeration and cast each into CustomValidationResult
// and conver them into enumeration of CustomModelValidationResult.
}
}
}
Finally we have to tell the DataAnnotationsModelValidatorProvider use this our CustomValidatableObjectAdapter in the Application_Start event of Global.asax.cs.
DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapterFactory((metadata, context) => new CustomValidatableObjectAdapter(metadata, context));
So you have to create a custom ValidationResult, custom ModelValidationResult and a custom ValidatableObjectAdapter.
I haven't tested this but I hope this will work. I may suggest a better and easier solution than this.

Why does creating a new ViewModel return the same data as the old ViewModel?

I'm just learning MVC3 now and this is really confusing me.
I have a ViewModel that contains some child ViewModels. Each of the ChildViewModels get rendered with a different Partial View, and when submitting execute a different action on the Controller. All the ChildViewModels should perform some custom validation on their data, and if successful it should move on to the next page. If the validation fails, it should simply return to the ParentView and display the errors.
[HandleError]
public class MyController: Controller
{
public ActionResult Index()
{
var viewModel = new ParentViewModel();
return View("ParentView", viewModel);
}
[HttpPost]
public ActionResult ChildViewModelB_Action(ChildViewModelB viewModel)
{
if (ModelState.IsValid)
{
return View("ChildViewModelB_Page2", viewModel);
}
else
{
// I'm having trouble returning to the ParentView and
// simply displaying the ChildViewModel's errors, however
// discovered that creating a new copy of the VM and displaying
// the ParentView again shows the existing data and any errors
// But why??
var vm = new ParentViewModel();
return View("ParentView", vm);
}
}
}
For example,
The page loads with 3 options.
User selects option B and fills out a form.
Upon submit, the child ViewModel B gets validated and fails.
Page returns to ParentView, with ChildB all filled out, however ChildB errors are now also showing.
Why does creating a new copy of the ParentViewModel display the ParentView with the same data as the original ParentViewModel?
And is there a different way I should be returning to the ParentView after doing server-side validation?
You need to clear the modelstate if you intend to modify values in your POST action
else
{
ModelState.Clear();
var vm = new ParentViewModel();
return View("ParentView", vm);
}
The reason for that is because Html helper such as TextBoxFor will first look in the modelstate when binding their values and after that in the model. And since the modelstate already contains the POSTed values, that's what's used => the model is ignored. This is by design.
This being said the correct thing to do in your case is to simply redirect to the GET action which already blanks the model and respect the Redirect-After-Post pattern:
else
{
return RedirectToAction("Index");
}
Why does creating a new copy of the ParentViewModel display the
ParentView with the same data as the original ParentViewModel?
Because the values of the fields are retrieved from the POSTed form and not from the model. That makes sense right? We don't want the user to show a form filled with different values from what they submitted.

MVC - one controller action with multiple model types/views or individual controller actions?

In my project I have a controller that allows you to create multiple letters of different types. All of these letter types are stored in the database, but each letter type has different required fields and different views.
Right now I have a route set up for the following URL: /Letters/Create/{LetterType}. I currently have this mapped to the following controller action:
public ActionResult Create(string LetterType)
{
var model = new SpecificLetterModel();
return View(model);
}
I also have a View called Create.cshtml and an EditorTemplate for my specific letter type. This all works fine right now because I have only implemented one Letter Type. Now I need to go ahead and add the rest but the way I have my action set up it is tied to the specific letter type that I implemented.
Since each Letter has its own model, its own set of validations, and its own view, what is the best way to implement these actions? Since adding new letter types requires coding for the model/validations and creating a view, does it make more sense to have individual controller actions:
public ActionResult CreateABC(ABCLetterModel model);
public ActionResult CreateXYZ(XYZLetterModel model);
Or is there a way I can have a single controller action and easily return the correct model/view?
You can do one of the following:
Have a different action method for each input. This is because the mvc framework will see the input of the action method, and use the default model binder to easily bind the properties of that type. You could then have a common private method that will do the processing, and return the view.
Assuming XYZLetterModel and ABCLetterModel are subclasses of some base model, your controller code could look like:
public class SomeController : Controller
{
private ISomeService _SomeService;
public SomeController(ISomeService someService)
{
_SomeService = someService;
}
public ViewResult CreateABC(ABCLetterModel abcLetterModel)
{
// this action method exists to allow data binding to figure out the model type easily
return PostToServiceAndReturnView(abcLetterModel);
}
public ViewResult CreateXYZ(XYZLetterModel xyzLetterModel)
{
// this action method exists to allow data binding to figure out the model type easily
return PostToServiceAndReturnView(xyzLetterModel);
}
private ViewResult PostToServiceAndReturnView(BaseLetterModel model)
{
if (ModelState.IsValid)
{
// do conversion here to service input
ServiceInput serviceInput = ToServiceInput(model);
_SomeService.Create(serviceInput);
return View("Success");
}
else
{
return View("Create", model);
}
}
}
The View code could look like:
#model BaseLetterModel
#if (Model is ABCLetterModel)
{
using (Html.BeginForm("CreateABC", "Some"))
{
#Html.EditorForModel("ABCLetter")
}
}
else if (Model is XYZLetterModel)
{
using (Html.BeginForm("CreateXYZ", "Some"))
{
#Html.EditorForModel("XYZLetter")
}
}
You would still have an editor template for each model type.
Another option is to have a custom model binder that figures out the type, based on some value in a hidden field, and then serializes it using that type.
The first approach is much more preferred because the default model binder works well out of the box, and it's a lot of maintenance to build custom model binders.

Can I add a ModelState.AddModelError from a Model class (rather than the controller)?

I want to display an error to the user in an ASP.MVC 3 input form using ModelState.AddModelError() so that it automatically highlights the right field and puts the error next to the particular field.
In most examples, I see ModelState.AddModelError() and if(ModelState.IsValid) placed right in the Controller. However, I would like to move/centralize that validation logic to the model class. Can I have the model class check for model errors and populate ModelState.AddModelError()?
Current Code:
// Controller
[HttpPost]
public ActionResult Foo(Bar bar)
{
// This model check is run here inside the controller.
if (bar.isOutsideServiceArea())
ModelState.AddModelError("Address", "Unfortunately, we cannot serve your address.");
// This is another model check run here inside the controller.
if (bar.isDuplicate())
ModelState.AddModelError("OrderNumber", "This appears to be a duplicate order");
if (ModelState.IsValid)
{
bar.Save();
return RedirectToAction("Index");
}
else
return View(bar)
}
Desired Code:
// Controller
[HttpPost]
public ActionResult Foo(Bar bar)
{
// something here to invoke all tests on bar within the model class
if (ModelState.IsValid)
{
bar.Save();
return RedirectToAction("Index");
}
else
return View(bar)
}
...
// Inside the relevant Model class
if (bar.isOutsideServiceArea())
ModelState.AddModelError("Address", "Unfortunately, we cannot serve your address.");
if (bar.isDuplicate())
ModelState.AddModelError("OrderNumber", "This appears to be a duplicate order");
If you are using MVC 3, you should checkout IValidatableObject, it's what you're after.
Scott Gu mentions it in his MVC3 Intro blog posting.
You can do something like this using custom data annotations or using RuleViolations like what they did in the NerdDinner example.
Perhaps you could create an error interface like IErrorHandler and pass that into a public method called Validate on you model class assuming its a partial class and you can seperate your data model from your rules.
With the interface you could create a class in your contoller that wraps the ModelState error handler. So the inteface might have AddError and in that metghod tou just delegate to your local modelstate.
So your method might be something like:
IErrorHandler errorHandler = CreateErrorHandler();
model.Validate(errorHandler);
if(errorHandler.IsValid())
... do something
You would use custom validation via the IValidatableObject interface on your model.
Custom validation example
If you're after least amount of code, a hacky way of doing it is to stuff that message into the Session from your View Model, then add that message from your Controller, for example:
// In Model (or elsewhere deep inside your app):
string propertyName = "Email";
string errorMessage = "This is the error message, you should fix it";
Session["DeepModelError"] = new KeyValuePair<string, string>(propertyName, errorMessage);
// In Controller
var model = new LoginModel();
if (Session["DeepModelError"] != null)
{
var deepError = (KeyValuePair<string, string>)Session["DeepModelError"];
if (deepError.Key == "Email" || deepError.Key == "Password")
{
ModelState.AddModelError(deepError.Key, deepError.Value);
Session["DeepModelError"] = null;
}
}
Let me reiterate that I realize that this is kind of hacky, but it's simple and it gets the job done...

Resources