Validating a view model after custom model binding - asp.net-mvc-3

I have a view model that implements IValidatableObject that contains a string and a collection of another view model, something like this:
public sealed class MainViewModel
{
public string Name { get; set; }
public ICollection<OtherViewModel> Others { get; set; }
}
My validation checks each object in Others against different rules using the contract provided by IValidatableObject:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var other in this.Others)
{
// validate or yield return new ValidationResult
}
}
Because of the complex structure of the real MainViewModel I have had to create a custom model binder which re-builds the model and assigns POST data to the relevant components. The problem that I'm getting is that nothing is getting validated resulting in validation errors at the context level as it violates certain database constraints and I'm not sure what I'm doing wrong - I assumed that ModelState.IsValid would invoke the Validate method on my view model but it doesn't seem to go down that way.
My model binder looks like this:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
int modelId = (int)controllerContext.RouteData.Values["id"];
// query the database and re-build the components of the view model
// iterate the POST data and assign to the model where necessary
// should I be calling something here to validate the model before it's passed to the controller?
return model;
}
Any help appreciated!
Validator.TryValidateObject
OK, seems I'm a little closer. I can now get my IValidatableObject method to run by adding the following to my custom model binder:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
Seems that Validator.TryValidateObject invokes the validation method and setting the last parameter to true causes it to validate all properties. However, I'm now stuck with getting the validationResults to the controller so they can be used in a meaningful way.

I should have realised that I could use the ModelState.AddModelError through a custom binder, I've managed to get this working correctly now by adding the following to my custom model binder before returning the model to the controller:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
foreach (var result in validationResults)
{
bindingContext.ModelState.AddModelError("", result.ErrorMessage);
}
}
return model;
This now returns a list of all errors to my page and the ModelState.IsValid check on my controller action is now returning false.

Paul's great answer can be refactored into a generic validate-and-convert to ModelState method as follows (e.g. in a helper or CustomModelBinder base). In addition, the bindings to the validated properties are retained.
public static void DoValidation(ModelBindingContext bindingContext,
IValidatableObject model)
{
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model,
new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
var resultsGroupedByMembers = validationResults
.SelectMany(_ => _.MemberNames.Select(
x => new {MemberName = x ?? "",
Error = _.ErrorMessage}))
.GroupBy(_ => _.MemberName);
foreach (var member in resultsGroupedByMembers)
{
bindingContext.ModelState.AddModelError(
member.Key,
string.Join(". ", member.Select(_ => _.Error)));
}
}
}

Related

IModelBinderProvider webapi equivalent

What would be equivalent of IModelBinderProvider in WEBAPI?
I have read this article by still not understand how to replace default binding or insert its own binding rules.
EDIT: Current 'classic' MVC model binder provider (implements IModelBinderProvider).
public IModelBinder GetBinder(Type modelType)
{
Type binderType;
lock (_syncLock)
{
// Check to see if a type was already bound.
if (_binders.ContainsKey(modelType))
binderType = _binders[modelType];
else
{
// Check the assembly and look for a <name>ModelBinder
Assembly assembly = Assembly.GetExecutingAssembly();
Type[] types = assembly.GetTypes();
// Convention over configuration
binderType = types.FirstOrDefault(t => t.Name == modelType.Name + "ModelBinder");
if (binderType != null)
_binders[modelType] = binderType;
}
}
if (binderType == null)
return null;
var binder = (IModelBinder) DependencyResolver.Current.GetService(binderType);
return binder;
}
So you can see I am trying to use dependency resolver for binder selection.
Also in configuration I replaced default binder with
DependencyResolver.SetResolver(new StructureMapDependencyResolver(ObjectFactory.Container));
At the end if I send parameter (as form or querystring) in example "LocationID" and I have webapi action like this
public HttpResponseMessage MyAction(MyCls obj) {
// do something
}
public class MyCls{
public string LocationID {get;set;}
}
I need MyCls to be bound and LocationID to get value of provided "LocationID" param.

Single property not getting bound on HttpPost

I'm working on the first MVC3 project at our company, and I've hit a block. No one can seem to figure out what's going on.
I have a complex Model that I'm using on the page:
public class SpaceModels : List<SpaceModel> {
public bool HideValidation { get; set; }
[Required(ErrorMessage=Utilities.EffectiveDate + Utilities.NotBlank)]
public DateTime EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
}
In the Controller, I create a SpaceModels object with blank SpaceModels for when Spaces get combined (this would be the destination Space).
// Need a list of the models for the View.
SpaceModels models = new SpaceModels();
models.EffectiveDate = DateTime.Now.Date;
models.DisplayEffectiveDate = true;
models.Add(new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
return View("CombineSpaces", models);
Then in the View, I am using that SpaceModels object as the Model, and in the form making a TextBox for the Effective Date:
#model Data.SpaceModels
#using (Html.BeginForm("CombineSpaces", "Space")) {
<div class="EditLine">
<span class="EditLabel LongText">
New Space Open Date
</span>
#Html.TextBoxFor(m => m.EffectiveDate, new {
size = "20",
#class = "datecontrol",
// Make this as a nullable DateTime for Display purposes so we don't start the Calendar at 1/1/0000.
#Value = Utilities.ToStringOrDefault(Model.EffectiveDate == DateTime.MinValue ? null : (DateTime?)Model.EffectiveDate, "MM/dd/yyyy", string.Empty)
})
#Html.ValidationMessageFor(m => m.EffectiveDate)
</div>
<hr />
Html.RenderPartial("_SpaceEntry", Model);
}
The Partial View that gets rendered iterates through all SpaceModels, and creates a containing the Edit fields for the individual SpaceModel objects. (I'm using the List to use the same Views for when the Spaces get Subdivided as well.)
Then on the HttpPost, the EffectiveDate is still back at it's DateTime.MinValue default:
[HttpPost]
public ActionResult CombineSpaces(SpaceModels model, long siteID, long storeID, DateTime? effectiveDate) {
// processing code
}
I added that DateTime? effectiveDate parameter to prove that the value when it gets changed does in fact come back. I even tried moving the rendering of the TextBox into the _SpaceEntry Partial View, but nothing worked there either.
I did also try using the #Html.EditorFor(m => m.EffectiveDate) in place of the #Html.TextBoxFor(), but that still returned DateTime.MinValue. (My boss doesn't like giving up the control of rendering using the #Html.EditorForModel by the way.)
There has to be something simple that I'm missing. Please let me know if you need anything else.
Looking at the source code for DefaultModelBinder, specifically BindComplexModel(), if it detects a collection type it will bind the individual elements but will not attempt to bind properties of the list object itself.
What model binding does is attempt to match the names of things or elements in the view to properties in your model or parameters in your action method. You do not have to pass all of those parameters, all you have to do is add them to your view model, then call TryUpdateModel in your action method. I am not sure what you are trying to do with SpaceModel or List but I do not see the need to inherit from the List. Im sure you have a good reason for doing it. Here is how I would do it.
The view model
public class SpacesViewModel
{
public DateTime? EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
public List<SpaceModel> SpaceModels { get; set; }
}
The GET action method
[ActionName("_SpaceEntry")]
public PartialViewResult SpaceEntry()
{
var spaceModels = new List<SpaceModel>();
spaceModels.Add(
new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
var spacesVm = new SpacesViewModel
{
EffectiveDate = DateTime.Now,
DisplayEffectiveDate = true,
SpaceModels = spaceModels
};
return PartialView("_SpaceEntry", spacesVm);
}
The POST action method
[HttpPost]
public ActionResult CombineSpaces()
{
var spacesVm = new SpacesViewModel();
// this forces model binding and calls ModelState.IsValid
// and returns true if the model is Valid
if (TryUpdateModel(spacesVm))
{
// process your data here
}
return RedirectToAction("Index", "Home");
}
And the view
<label>Effective date: </label>
#Html.TextBox("EffectiveDate", Model.EffectiveDate.HasValue ?
Model.EffectiveDate.Value.ToString("MM/dd/yyyy") : string.empty,
new { #class = "datecontrol" })
Sometimes you need to explicitly bind form data using hidden fields such as
#Html.HiddenField("EffectiveDate", Model.EfectiveDate.)
In order to bind the properties of the SpaceModel object you can add individual properties such as SiteID to the view model or add a SpaceModel property for a single SpaceModel. If you want to successfully bind a complex model, add it as a Dictionary populated with key-value pairs rather than a List. You should then add the dictionary to the view model. You can even add a dictionary of dictionaries for hierarchical data.
I hope this helps :)

ASP.NET MVC 3 Viewmodel Pattern

I am trying to work out the best way of using a viewmodel in the case of creating a new object.
I have a very simple view model that contains a contact object and a select list of companies.
private ICompanyService _Service;
public SelectList ContactCompanyList { get; private set; }
public Contact contact { get; private set; }
public ContactCompanyViewModel(Contact _Contact)
{
_Service = new CompanyService();
contact = _Contact;
ContactCompanyList = GetCompanyList();
}
private SelectList GetCompanyList()
{
IEnumerable<Company> _CompanyList = _Service.GetAll();
return new SelectList(_CompanyList, "id", "name");
}
I then have contact controller that uses this viewmodel and enable me to select a related company for my contact.
[Authorize]
public ActionResult Create()
{
return View(new ContactCompanyViewModel(new Contact()));
}
My issue is with the create method on the controller.
[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Contact _Contact)
{
try
{
_Service.Save(_Contact);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
The problem is that the view returns an empty contact object, but! the company id is populated, this is because the dropdown list explicitly declares its field name.
#Html.DropDownList("parent_company_id",Model.ContactCompanyList)
The standard html form fields pass the objects values back in the format of contact.forename when using the HTML.EditorFor helper...
#Html.EditorFor(model => model.contact.forename)
I can access them if I use a FormCollection as my create action method paremeter and then explicitly search for contact.value but I cannot use a Contact object as a parameter to keep my code nice and clean and not have to build a new contact object each time.
I tried passing the actual view model object back as a parameter but that simply blows up with a constructor error (Which is confusing seeing as the view is bound to the view model not the contact object).
Is there a way that I can define the name of the Html.EditFor field so that the value maps correctly back to the contact object when passed back to the create action method on my controller? Or Have I made some FUBAR mistake somewhere (that is the most likely explanation seeing as this is a learning exercise!).
Your view model seems wrong. View models should not reference any services. View models should not reference any domain models. View models should have parameterless constructors so that they could be used as POST action parameters.
So here's a more realistic view model for your scenario:
public class ContactCompanyViewModel
{
public string SelectedCompanyId { get; set; }
public IEnumerable<SelectListItem> CompanyList { get; set; }
... other properties that the view requires
}
and then you could have a GET action that will prepare and populate this view model:
public ActionResult Create()
{
var model = new ContactCompanyViewModel();
model.CompanyList = _Service.GetAll().ToList().Select(x => new SelectListItem
{
Value = x.id.ToString(),
Text = x.name
});
return View(model);
}
and a POST action:
[HttpPost]
public ActionResult Create(ContactCompanyViewModel model)
{
try
{
// TODO: to avoid this manual mapping you could use a mapper tool
// such as AutoMapper
var contact = new Contact
{
... map the contact domain model properties from the view model
};
_Service.Save(contact);
return RedirectToAction("Index");
}
catch
{
model.CompanyList = _Service.GetAll().ToList().Select(x => new SelectListItem
{
Value = x.id.ToString(),
Text = x.name
});
return View(model);
}
}
and now in your view you work with your view model:
#model ContactCompanyViewModel
#using (Html.BeginForm())
{
#Html.DropDownListFor(x => x.SelectedCompanyId, Model.CompanyList)
... other input fields for other properties
<button type="submit">Create</button>
}

Can I reuse a remote validation action in MVC3

I am using a Remote validation attribute on my view model to validate a Bank Account that is specified for my Company:
ViewModel:
[Remote("CheckDefaultBank", "Company")]
public string DefaultBank
{
This in the controller I have:
[HttpGet]
public JsonResult CheckDefaultBank(string defaultBank)
{
bool result = BankExists(defaultBank);
return Json(result, JsonRequestBehavior.AllowGet);
}
That all works well. But, I have two other banks related to my company as well. However, when the remote validation js calls the action it uses a parameter mactching the field name of "DefaultBank"... so I use that as a parameter in my action.
Is there some attribute I can add in the view so that it will use a parameter of say "bankId" on the ajax get so I don't need an action for each field which are basically exactly the same?
The goal here is to eliminate now having to have this in my controller:
[HttpGet]
public JsonResult CheckRefundBank(string refundBank)
{
bool result = BankExists(defaultBank);
return Json(result, JsonRequestBehavior.AllowGet);
}
[HttpGet]
public JsonResult CheckPayrollBank(string payrollBank)
{
bool result = BankExists(defaultBank);
return Json(result, JsonRequestBehavior.AllowGet);
}
I was hoping I could do something like this in the view:
#Html.EditorFor(model => model.DefaultBank, new { data-validate-parameter: bankId })
This way I could just use the same action for all of the Bank entries like:
[HttpGet]
public JsonResult CheckValidBank(string bankId)
{
bool result = BankExists(bankId);
return Json(result, JsonRequestBehavior.AllowGet);
}
Possible?
For just such a situation, I wrote a RemoteReusableAttribute, which may be helpful to you. Here is a link to it: Custom remote Validation in MVC 3
Since MVC uses the default model binder for this, just like a normal action method. You could take a FormsCollection as your parameter and lookup the value. However, I personally would find it much easier to just use several parameters to the function, unless you start having dozens of different parameters.
You could also write a custom model binder, that would translate the passed parameter to a generic one.
Consider encapsulating the logic, "BankExists" in this case into a ValidationAttribute (Data Annotations Validator). This allows other scenarios as well.
Then use a wrapper ActionResult like the one below, which lets you pass in any validator.
[HttpGet]
public ActionResult CheckRefundBank(string refundBank)
{
var validation = BankExistsAttribute();
return new RemoteValidationResult(validation, defaultBank);
}
Here is the code for the ActionResult that works generically with Validators.
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
public class RemoteValidationResult : ActionResult
{
public RemoteValidationResult(ValidationAttribute validation, object value)
{
this.Validation = validation;
this.Value = value;
}
public ValidationAttribute Validation { get; set; }
public object Value { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var json = new JsonResult();
json.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
if (Validation.IsValid(Value))
{
json.Data = true;
}
else
{
json.Data = Validation.FormatErrorMessage(Value.ToString());
}
json.ExecuteResult(context);
}
}
As an extra enhancement consider creating a Controller Extension method to dry up your return call even more.

ViewBag property value in DropDownListFor instead of Model property value

We found strange behaviour in DropDownListFor (ASP.NET MVC3 release). It selects ViewBag property value instead of Model property value in dropdown.
Model:
public class Country {
public string Name { get; set; }
}
public class User {
public Country Country { get; set; }
}
Controller Index action:
ViewBag.CountryList = new List<Country> { /* Dropdown collection */
new Country() { Name = "Danmark" },
new Country() { Name = "Russia" } };
var user = new User();
user.Country = new Country(){Name = "Russia"}; /* User value */
ViewBag.Country = new Country() { Name = "Danmark" }; /* It affects user */
return View(user);
View:
#Html.EditorFor(user => user.Country.Name)
#Html.DropDownListFor(user => user.Country.Name,
new SelectList(ViewBag.CountryList, "Name", "Name", Model.Country), "...")
It will show text box with "Russia" value and dropdown with "Danmark" value selected instead of "Russia".
I didn't find any documentation about this behaviour. Is this behaviour normal? And why is it normal? Because it is very hard to control ViewBag and Model properties names.
This sample MVC3 project sources
I'm not so sure why this decision was made, but it was happened because MVC framework tried to use the ViewData-supplied value before using the parameter-supplied value. That's why ViewBag.Country override parameter-supplied value Model.Country.
That was how it was written in MVC framework in the private method SelectInternal.
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData) {
if (defaultValue == null) {
defaultValue = htmlHelper.ViewData.Eval(fullName);
}
}
if (defaultValue != null) {
IEnumerable defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
IEnumerable<string> values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture);
HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
List<SelectListItem> newSelectList = new List<SelectListItem>();
foreach (SelectListItem item in selectList) {
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
selectList = newSelectList;
}
This code defaultValue = htmlHelper.ViewData.Eval(fullName); tried to get the value from ViewData and if it can get the value, it will override the supplied parameter selectList with new list.
Hope it can help. Thanks.
side-node: ViewBag is just a dynamic wrapper class of ViewData.
The following line from your action method is what is confusing the code:
ViewBag.Country = new Country() { Name = "Danmark" }; /* It affects user */
That's because the html helpers look into a few different places to pick up values for the generated controls. In this case ViewData["Country"] is clashing with ModelState["Country"] Rename that property to something else and everything should work.

Resources