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).
Related
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);
}
I'm trying to list the items from my database into my view but I'm getting null back.
I know the connection must be working to a certain extent because in my database the tables didn't exist but once I ran my program it did create the tables. However when I add content into my table my view still returns NULL.
Also, haven't touched the Review table yet, just worried about getting Restaurants working.
Restaurant.cs
namespace OdeToFood.Models
{
public class Restaurant
{
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public string Country { get; set; }
public ICollection<RestaurantReview> Reviews { get; set; }
}
}
OdeToFood.cs
namespace OdeToFood.Models
{
public class OdeToFoodDb : DbContext
{
public DbSet<Restaurant> Restaurants { get; set; }
public DbSet<RestaurantReview> Reviews { get; set; }
}
}
Controller
OdeToFoodDb _db = new OdeToFoodDb();
public ActionResult Index()
{
var model = _db.Restaurants.ToList();
return View();
}
Index.cshtml
#model IEnumerable<OdeToFood.Models.Restaurant>
#{
ViewBag.Title = "Home Page";
}
#{
if (Model != null)
{
foreach (var item in Model)
{
<div>
<h4>#item.Name</h4>
<div>#item.City, #item.Country</div>
<hr />
</div>
}
}
else
{
<h1>Null</h1>
}
}
You need to pass to model back to the view.
OdeToFoodDb _db = new OdeToFoodDb();
public ActionResult Index()
{
var model = _db.Restaurants.ToList();
return View(model);
}
You never actually send the model to the view. Pass it as an argument:
OdeToFoodDb _db = new OdeToFoodDb();
public ActionResult Index()
{
var model = _db.Restaurants.ToList();
return View(model);
}
Additionally, it's generally a good idea not to create database contexts in a shared scope. Keep the context as close to where it's used as possible and only expand its scope when you really need to. Something like this:
public ActionResult Index()
{
using (var _db = new OdeToFoodDb())
{
var model = _db.Restaurants.ToList();
return View(model);
}
}
Database contexts/connections in a shared scope is just asking for problems unless you pay close attention to what you're doing. As the code gets more complex, it becomes more likely that other methods will try to use it and it may be in an unknown state at that time.
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);
}
}
I have read many times experts of MVC saying that if I were to use a SelectList, it's best to have a IEnumerable<SelectList> defined in my model.
For example, in this question.
Consider this simple example:
public class Car()
{
public string MyBrand { get; set; }
public IEnumerable<SelectListItem> CarBrands { get; set; } // Sorry, mistyped, it shoudl be SelectListItem rather than CarBrand
}
In Controller, people would do:
public ActionResult Index()
{
var c = new Car
{
CarBrands = new List<CarBrand>
{
// And here goes all the options..
}
}
return View(c);
}
However, from Pro ASP.NET MVC, I learned this way of Creating a new instance.
public ActionResult Create() // Get
{
return View()
}
[HttpPost]
public ActionResult Create(Car c)
{
if(ModelState.IsValid) // Then add it to database
}
My question is: How should I pass the SelectList to View? Since in the Get method there is no model existing, there seems to be no way that I could do this.
I could certainly do it using ViewBag, but I was told to avoid using ViewBag as it causes problems. I'm wondering what are my options.
You could create a ViewModel that has all the properties of Car which you want on your form then make your SelectList a property of that ViewModel class
public class AddCarViewModel
{
public int CarName { get; set; }
public string CarModel { get; set; }
... etc
public SelectList MyList
{
get;
set;
}
}
Your controller will look like
public ActionResult Create() // Get
{
AddCarViewModel model = new AddCarViewModel();
return View(model)
}
[HttpPost]
public ActionResult Create(AddCarViewModel c)
{
if(ModelState.IsValid) // Then add it to database
}
MarkUp
#Html.DropDownListFor(#model => model.ListProperty, Model.MyList, ....)
Easy way, this is a copy of my code
without model
In the controller
ViewBag.poste_id = new SelectList(db.Postes, "Id", "designation");
In the view
#Html.DropDownList("poste_id", null," -- ", htmlAttributes: new { #class = "form-control" })
I have a "New user" form both for admins and for regular users. Both form use the RegisterModel
public class RegisterModel
{
[Required]
public string Name { get; set; }
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
The difference is that on my front end "New user" page I want users to provide their own password. But in back end, I want the system to generate the password.
Since I use the same RegisterModel for both forms, I get a validateion error in the back end saying Password is required..
I thought, I could solve this by adding this to my controller:
[HttpPost]
public ActionResult New(RegisterModel model)
{
model.Password = Membership.GeneratePassword(6, 1);
if (TryValidateModel(model))
{
// Do stuff
}
return View(model);
}
But I still get the error message Password is required.. Why is this the issue when I do call TryValidate in my controller?
What would be best practice for this issue, create a separate RegisterModelBackEnd or are there any other solutions to this?
When updating model manually, you do not need to use it as parameter in Action. Also, use this overload that lets you specify only the properties on which binding will occur.
protected internal bool TryUpdateModel<TModel>(
TModel model,
string[] includeProperties
)
where TModel : class
So, the working code will be
[HttpPost]
public ActionResult New()
{
RegisterModel model = new RegisterModel();
model.Password = Membership.GeneratePassword(6, 1);
if (TryValidateModel(model, new string[] {"Name", "Email"}))
{
// Do stuff
}
return View(model);
}
You can make this even simpler, using BindAttribute
[HttpPost]
public ActionResult New([Bind(Exlude="Password")]RegisterModel model)
{
if(ModelState.IsValid)
{
model.Password = Membership.GeneratePassword(6, 1);
// Do Stuff
}
return View(model);
}
And finally simplest and the best way
Define separate view models