Form validation and passing model back to view - asp.net-mvc

I have setup validation in my viewmodel such as the below:
[Required(ErrorMessage = "This field is required.")]
[StringLength(25, MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password)]
[System.Web.Mvc.CompareAttribute("Password", ErrorMessage = "Password must be the same")]
public string ConfirmPassword { get; set; }
When I submit the form I check if ModelState.IsValid and if its not valid then return the original view but by doing this I lose the original data I had in my model.
[HttpPost]
public ActionResult Form(MemberAddViewModel viewModel, string returnUrl)
{
if (ModelState.IsValid)
{
...
}
return View("Form", viewModel);
}
I would have expected the viewModel to be passed back to the original View but it seems only model items populated in the view are. What is best practice for this? Hidden fields / Session data?

To understand why you have to rebuild parts of the model, you need to think about what's going on under the hood when the model binder is passed the data from your view. A SelectList is a perfect example of this.
Let's say you have a view model as follows:
public class EmployeesViewModel
{
public int EmployeeId { get; set; }
public SelectList Employees { get; set; }
// other properties
}
Here, EmployeeId represents the Id of the selected Employee from the SelectList. So let's assume you have a controller action like this, which populates the SelectList and passes the data to the view:
public ActionResult Index()
{
var model = new EmployeesViewModel();
model.Employees = new SelectList(/* populate the list */);
return View(model);
}
Now let's assume a user comes along, navigates to this view, chooses an employee from the list, and POSTs the data back to the server. When this happens, the only thing that gets submitted from that form, is the Id of the employee. There's no need for HTTP to transport all of the other options from the SelectList to the server, because a) they haven't been selected and b) you have that data on the server already.

Not sure for I undertandood your question... This is how i populate lost fields: i divide model population into 2 parts: for editable props (elements that being posted back to server) and non-editable (that are getting lost on postback)
// View model
public class MyModel
{
public MyModel() { }
public MyModel(Entity e, ContractTypes[] ct)
{
// populate model properties from entity
ContractTypeId = e.ContractTypeId;
// and call magic method that'll do the rest my model needs
PopulateNonEditableFields(ct);
}
public void PopulateNonEditableFields(
Dictionary<int, string> ContractTypes [] ct)
{
// populate dictionaries for DDLs
ContractTypesList = new SelectList(..., ct,...);
}
// model properties
public ContractTypeId { get; set; }
public SelectList ContractTypesList { get; set; }
}
// controller action
[HttpPost]
public ActionResult Action(MemberAddViewModel viewModel)
{
if (ModelState.IsValid)
{
...
}
// user input stays as-is but need to populate dictionaries and evrithing
// that was lost on postback
viewModel.PopulateNonEditableFields(context.ContractTypes.GetAll());
return View("Form", viewModel);
}

As I understand it, you have data in the view model which is not posted back through the form. There can be many valid reasons why this is the case.
Another alternative is to always create the view model manually, and then update it using the posted back values through a call to TryUpdateModel. Calling TryUpdateModel will do two things: set the model's public properties using the controller's value provider, then runs validation checks on the model.
[HttpPost]
public ActionResult Action(int id, string returnUrl)
{
MemberAddViewModel viewModel = CreateViewModel(id); // Populates with
// intial values
if(TryUpdateModel(viewModel))
{
// If we got here, it passed validation
// So we continue with the commit action
// ...
}
else // It failed validation
{
return View("Form", viewModel);
}
}

Related

Adding values to model and passing trough ModelState vaidation

I am trying to understand what would be the best approach to my problem. Let's say I have a model like this:
public class Customer
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
[Required]
public int StoreId { get; set;}
[Required]
public DateTime UpdatedAt {get; set;}
}
and I have an API controller that will have a method like the following to insert a new customer in the database:
public IHttpActionResult Insert(Customer customer)
{
customer.StoreId = 5; //just for example
customer.UpdatedAt = DateTime.Now; //again, just as example
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Customers.Add(customer);
db.SaveChanges();
return Ok(customer.Id);
}
Now as you can see in the method, let's assume the StoreId and the UpdatedAt fields won't be included in the original http request posted to this method, because those values need to be calculated and assigned at the server side (let's say basically the client is not supposed to send those values to the server side method). In this case, the ModelState won't be valid anymore, as it is missing two required fields.
One way you can get around it, is to clear the errors on the model state one by one, by doing:
ModelState["Store.Id"].Errors.Clear();
ModelState["UpdatedBy"].Errors.Clear();
and then doing the validation, but it doesn't look a good way especially if you have many fields that need to be taken care of on the server side.
What are the better solutions?
The good way ? Create a view model specific to the view and have only properties which the view is supposed to provide.
public class CustomerVm
{
public int Id { get; set; }
public string Name { get; set; }
}
In your GET action, send an object of this to the view
public ActionResult Edit(int id) //Or even Create
{
var vm=new CustomerVm { Id=id, Name="Scott to be edited"};
return View(vm);
}
Now your view will be strongly typed to this
#model CustomerVm
#using(Html.BeginForm())
{
#Html.HiddenFor(s=>s.Id)
#Html.TextBoxFor(s=>s.Name)
<input type="submit" />
}
and in your HttpPost action, Use the same view model as your method parameter, read the property values and use that
[HttpPost]
public ActionResult Create(CustomerVm model)
{
var customerEntity = new Customer { Name= model.Name };
//Stuff server should set goes now
customerEntity.StoreId = 23;
customerEntity.UpdatedAt = DateTime.Now;
db.Customers.Add(customerEntity);
db.SaveChanges();
return Ok(customerEntity.Id);
}

ModelState, TempData, ViewModel issue - new viewmodel value automatically gets overridden

This problem is best explained with code:
public async Task<ActionResult> Index()
{
if (TempData.ContainsKey("ModelState"))
{
ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);
var viewModelA= new ViewModelA
{
FirstName = "John"
}
return View(viewModelA);
}
}
[HttpPost]
public async Task<ActionResult> IndexPostActionMethod(ViewModelB model)
{
if (ModelState.IsValid)
{
var result = await DoSomeStuff();
if (result.Succeeded)
{
DoSomeOtherStuff();
}
else
{
AddErrors(result);
}
}
TempData["ModelState"] = ModelState;
return RedirectToAction("Index");
}
The problem here is that when I post to IndexPostActionMethod and DoSomeStuff() returns false for some reason, the form/page is rediplayed by redirecting to Index and saving the ModelState in TempData during that redirect and then once the form/page is redisplayed, the value from ModelState (or where does it get the value from?) gets set as the value for a #Html.TextBoxFor(m => m.FirstName) and not the value that I set here: var viewModelA = new ViewModelA { FirstName = "John"}. Why can't I override the value and where does it get it from? It's like the framework is automatically updating viewModelA with whatever is in the ModelStatDictionary eventhough I create a new ViewModelA with a new value.
Don't do this. The way you should be handling a GET-POST-Redirect cycle is:
public async Task<ActionResult> Index()
{
var model = new ViewModel
{
FirstName = "John"
}
return View(model);
}
[HttpPost]
public async Task<ActionResult> Index(ViewModel model)
{
if (ModelState.IsValid)
{
var result = await DoSomeStuff();
if (result.Succeeded)
{
DoSomeOtherStuff();
}
else
{
AddErrors(result);
}
}
return View(model);
}
ModelState rules all. Regardless of what you pass to the view explicitly, if the value is present in ModelState it will take precedence. Using TempData to pass the ModelState to another view is such a code smell, "smell" doesn't even really cover it.
If your only goal is to use one view model for the GET action and another, different, view model for the POST action, then you should really just stop and ask yourself seriously why you need to do that. In all the years I've worked with MVC, I've never had to do that, never needed to do that, and I can't imagine a scenario where I would need to do that.
UPDATE
So, based on the scenario you laid out in the comments, I would design it like this:
public class IndexViewModel
{
public SomeTabViewModel SomeTab { get; set; }
public AnotherTabViewModel AnotherTab { get; set; }
public FooTabViewModle FooTab { get; set; }
}
public class SomeTabViewModel
{
[Required]
public string SomeRequiredField { get; set; }
}
public class AnotherTabViewModel
{
[Required]
public string AnotherRequiredField { get; set; }
}
public class FooTabViewModel
{
[Required]
public string FooRequiredField { get; set; }
}
Basically, you break up the individual POST pieces into view models that altogether make up one big view model. Then, model validation only follows actual internal view model instances of the container. So, for example if you postback to FooTab.FooRequiredField only, then only FooTab is instantiated. The other two view model properties will remain null and will not be validated internally (i.e. the fact that they have required fields doesn't matter).

ASP.Net MVC4 ViewModel null properties and HttpPost

I have the following ViewModel:
public class MyViewModel
{
public int Id { get; set; }
public string Title { get; set; }
public int ParentClassId { get; set; }
public List<AnotherClass> AnotherClassItems { get; set; }
}
And in the view I only have a form input for Title, and a list of AnotherClassItems that aren't editable - they are just used to display a list of items related to the class. All of the properties are set when the 'edit' view loads, but when the view does a post, the ParentClassId & AnotherClassItems lists are null. Here is the HttpPost ActionResult from the controller:
[HttpPost]
public ActionResult Edit(FormCollection collection, MyViewModel myviewmodel)
{
if (ModelState.IsValid)
{
//myviewmodel.ParentClassId and myviewmodel.AnotherClassItems are null??
}
return View(myviewmodel);
}
Is there a way to pass the ParentClassId & AnotherClassItems properties without having them as form inputs in the view? Or should I use the Viewbag for this?
Remember, the form only posts back HTML input fields. So if you did not make any text boxes or hidden boxes they will not post to your Edit action.
The correct way to handle this is by passing the ID as a hidden field, then pulling the original values from your database/repository.
Then apply the new values for Title and AnotherClassItems to your model and save to the repository/database.

How to include all custom object property in MVC 4 HttpPost

I have a view model with a custom object. On the initial get, I populate Foo and use a couple of Foo's properties.
On the post, I find that Foo on the view model is null.
I could add to my view, #Html.HiddenFor(x => x.Foo.Id) which could ensure that a Foo is populated with at least an Id, but then I could need to add similar code for all of the properties.
Is there a way to send back the complete object?
public class RequestModel
{
public Foo Foo{ get; set; }
[Required]
[Display(Name = "Comment")]
public string Comment { get; set; }
}
Controller
public ActionResult Index(int? id)
{
//Populate Foo here using EF and add it to the model
var model = new RequestModel { Foo = foo };
return View(model);
}
[HttpPost]
public ActionResult Index(int? id, RequestModel model)
{
return View(model);
}
View
#Html.DisplayTextFor(m=>m.Application.Name)
etc
Add a view model with the properties you want to your solution. Put your validations etc on it and use that to move your data between your page and controller then map the properties to your EF object.

Validate a model in viewmodel?

I wonder if there is a way to validate just one of my models in the viewmodel send it to my action? I use the DataAnnotations as validate rules.
Like the if (!ModelState.IsValid)
let me know if the question is unclear and I will edit for a better explination
EDIT
my viewmodel looks like this
public class CompaniesViewModel
{
public Core.Model.Customer Company { get; set; }
public IEnumerable<SelectListItem> Items { get; set; }
public Core.Model.Meeting Meeting { get; set; }
}
What I want to do in this particular situation is to validate just Customer. I cant do the ModelState.IsValid then all get validated. So how can I do to just validate one of them like customer in this case. Hope this was more clear
There are a number of different ways you can do this. The first is to add a property called IsValid that checks the property. So something like:
public class Company
{
public bool IsValid
{
get { return GetValid() }
}
private bool IsValid()
{
if ( Some check here )
return false;
}
}
[HttpPost]
public ActionResult SomeAction(CompaniesViewModel model)
{
if (model.Company.IsValid)
{
}
}
However a better solution IMO would be just to post the Company to your controller rather than your entire view model. Just because your passing a view model to a view it doesn't mean that you need to post the entire view model back. When you create your HTML form specify only the properties you want to post back to your controller. So for example your controller would become:
[HttpPost]
public ActionResult SomeAction(Company company)
{
if (Model.IsValid)
{
}
}
Now when you check if Model.IsValid it just check company as that is all you've passed back to the controller.
At server side, you can try the ValidateModel(object) method, like in TryValidateModel(CompaniesViewModel.Company).
If you have enabled client sided validation, you need to post only the relevant entity. If you want to post all entities, but you need to validate only one, you can consider the following:
either removing the rules, using javascript ASP .NET MVC Disable Client Side Validation at Per-Field Level
or creating a Data-Transfer-Object, ie a View Model which has NO link to the Model, but reproduces the entities you want with the validation rules you want having applied in this scenario. Of course, then, you'll need in your controller or a model binder some way to bind from your ViewModel to your Model entities.
You can separate Customer Model to another class in your ViewModel and map that in Controller to a existing/new Customer:
public class CompaniesViewModel
{
public Company Company { get; set; }
public IEnumerable<SelectListItem> Items { get; set; }
public Core.Model.Meeting Meeting { get; set; }
}
//Validations for Company go here:
public class Company
{
public string CompanyId { get; set; }
[Required]
public string CompanyName { get; set; }
}

Resources