As I understand from the question below it should be possible to use different models for Get and Post actions. But somehow I'm failing to achieve just that.
What am I missing?
Related question: Using two different Models in controller action for POST and GET
Model
public class GetModel
{
public string FullName;
public string Name;
public int Id;
}
public class PostModel
{
public string Name;
public int Id;
}
Controller
public class HomeController : Controller
{
public ActionResult Edit()
{
return View(new GetModel {Id = 12, Name = "Olson", FullName = "Peggy Olson"});
}
[HttpPost]
public ActionResult Edit(PostModel postModel)
{
if(postModel.Name == null)
throw new Exception("PostModel was not filled correct");
return View();
}
}
View
#model MvcApplication1.Models.GetModel
#using (Html.BeginForm()) {
#Html.EditorFor(x => x.Id)
#Html.EditorFor(x=>x.Name)
<input type="submit" value="Save" />
}
Your models aren't using proper accessors so model binding doesn't work. Change them to this and it should work:
public class GetModel
{
public string FullName { get; set; }
public string Name { get; set; }
public int Id { get; set; }
}
public class PostModel
{
public string Name { get; set; }
public int Id { get; set; }
}
A bit of clarification
GET and POST controller actions can easily use whatever types they need to. Actually we're not talking about models here. Model is a set of classes/types that represent some application state/data. Hence application or data model.
What we're dealing here are:
view model types
action method parameter types
So your application model is still the same. And GetModel and PostModel are just two classes/types in this model. They're not model per-se.
Different types? Of course we can!
In your case you're using a view model type GetModel and then pass its data to PostModel action parameter. Since these two classes/types both have properties with same matching names, default model binder will be able to populate PostModel properties. If property names wouldn't be the same, you'd have to change the view to rename inputs to reflect POST action type property names.
You could as well have a view with GetModel type and then post action with several different prameters like:
public ActionResult Edit(Person person, IList<Address> addresses)
{
...
}
Or anything else. You'll just have to make sure that post data can be bound to these parameters and their type properties...
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 have a ViewModel which contains two other ViewModels (stripped for brevity):
class SmallViewModel1
{
public string Item { get; set; }
}
class SmallViewModel2
{
public string Item { get; set;}
}
class BigViewModel {
public SmallViewModel1 Model1 { get; set; }
public SmallViewModel2 Model2 { get; set; }
}
I then have a View which accepts BigViewModel as its model. The view has two forms which POST to two different actions.
#model BigViewModel
#using (Html.BeginForm("Action1","Controller",FormMethod.Post))
{
#Html.TextBoxFor(t=>t.Model1.Item)
}
#using (Html.BeginForm("Action2","Controller",FormMethod.Post))
{
#Html.TextBoxFor(t=>t.Model2.Item)
}
Then in the controller I am trying to do something like this:
public ActionResult Action1(SmallViewModel1 model)
{
....
}
public ActionResult Action2(SmallViewModel2 model)
{
....
}
The issue I am running into is if I use the 'For' controls (EditorFor, TextBoxFor, etc) my POSTed model is null. I think this is because it expects me to POST the full ViewModel (BigViewModel) and it names the inputs expecting this: <input name="Model1.Item". To get around this I have been using the non 'For' control and just setting the name to the SmallViewModel's property, for example: #Html.TextBox("Item"). This maps correctly to the controller and everything ends up working.
My questions is am I doing this correctly? I've been googling around for a bit and haven't found anything but I feel like there is a better, or more 'best-practice' way to do this.
Your Controller should be like this
public ActionResult Action1(BigViewModel model)
{
....
}
public ActionResult Action2(BigViewModel model)
{
....
}
As you have taken BigViewModel, When you submit it should have same model in parameter
Alternatively you can use the Prefix property of the BindAttribute
public ActionResult Action1([Bind(Prefix="Model1")]SmallViewModel1 model)
{
....
}
public ActionResult Action2([Bind(Prefix="Model2")]SmallViewModel2 model)
{
....
}
I am new to asp.net, there are some questions on stack overflow but they don't fulfill my purpose. My question is..
How would I implement view model for the following two models?
public class model1
{
int student-id{ get;set}
string student-name{get; set;}
}
public class model2
{
int course-code{get; set;}
string course-name{get; set;}
}
Now I want to write a view model that could pass to a view and this view displays student-name and corresponding course-names.
Note: a student can enrolled in more than one course.
First of all you should modify your model. Student and courses have to be related. You can implement these relations like:
public class Student
{
public int Id { get; set }
public string Name { get; set; }
public ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Code { get; set; }
public string Name{ get; set; }
public ICollection<Student> Students { get; set; }
}
After - you create view model. View model class must contain only what you actually need in you view. In you case - student and courses names. You can consider several options here. If you want just to display all course names in one line you can build you StudentViewModel like this:
public class StudentViewModel
{
public string Name { get; set; }
// In this case you can just join all courses' names to one string using string.Join(", ")
public string Courses { get; set; }
}
... or like this - if you want courses' names separated (to use them in some select or list html element). But you can create JoinedCources property which will return courses' names joined into one string.
public class StudentViewModel
{
public string Name { get; set; }
public ICollection<string> Courses { get; set; }
public string JoinedCources {
get {
return string.Join(", ", Courses);
}
}
}
Note: this is view model for only one student! If you want to display view which shows you the list of students and their courses you should either create new view model with property which is collection of StudentViewModel or in your view define model like #model ICollection<StudentViewModel> instead of #model StudentViewModel.
Now you have to map your model to view model. For example in your controller action when you get your student from database (or any other data source - file or web service):
public ActionResult StudentDetails(int studentId)
{
var student = _dataSource.GetStudent(studentId);
var model = AutoMapper.Mapper.Map<StudentViewModel>(student);
return View(model);
}
Now few words about mapping. AutoMapper is external class library you should definitely get to learn about if you want to work with view models and mapping in the future. It will help you simplify action method code and make it more readable. But since you're new to ASP. Net you can implement mapping by your self for the first time. For example like below:
public ActionResult StudentDetails(int studentId)
{
var student = _dataSource.GetStudent(studentId);
var model = new StudentViewModel()
{
Name = student.Name,
Courses = student.Courses.Select(c => c.Name)
}
return View(model);
}
public class StudentViewModel
{
public string Name{get;set;}
public int StudentId{ get;set}
public List<model2> Courses
}
You can consider combining the two models into the above view model if the goal is to display a student with course info.
In my action I use a InsertPerson model that has the following properties:
pulbic class InsertPerson
{
[Required]
public string Name{get;set;}
}
and I have a DTO model that is passed to my repository:
public class PersonDto
{
public int Id{get;set;}
public string Name{get;set;}
public string LastName{get;set;}
}
my action:
[HttpPost]
public ActionResult Create(InsertPerson insertPerson)
{
if(ModelState.IsValid){
var personDto = new PersonDto();
UpdateModel(personDto)
//UpdateModel(personDto, "insertPerson") I've tried this too
}
}
why it doesn't work(after UpdateModel all properties is still null)?
Is there any way to update my personDto using UpdateModel?
I know about AutoMapper but I think it's boring to use in controller.
My view:
#model Help_Desk.ViewModel.InsertPersonViewModel
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.InsertPerson.Name)
<button type="submit">Save</button>
}
and my viewmodel:
public class InsertPersonViewModel
{
public InsertPerson InsertPerson{ get; set; }
}
Ok, now we see the problem. The information that was missing is that you were using a View Model, and the View Model creates a "container" of sorts that must be accounted for in the UpdateModel.
What's happening is that UpdateModel is trying to literally update "InsertPerson.Name" in your personDto object. Since that doesn't exist, it's not working.
You can fix this by specifying UpdateModel(personDto, "InsertPerson")
Ok, this is weird. I cannot use BindAttribute's Include and Exclude properties with complex type nested objects on ASP.NET MVC.
Here is what I did:
Model:
public class FooViewModel {
public Enquiry Enquiry { get; set; }
}
public class Enquiry {
public int EnquiryId { get; set; }
public string Latitude { get; set; }
}
HTTP POST action:
[ActionName("Foo"), HttpPost]
public ActionResult Foo_post(
[Bind(Include = "Enquiry.EnquiryId")]
FooViewModel foo) {
return View(foo);
}
View:
#using (Html.BeginForm()) {
#Html.TextBoxFor(m => m.Enquiry.EnquiryId)
#Html.TextBoxFor(m => m.Enquiry.Latitude)
<input type="submit" value="push" />
}
Does not work at all. Can I only make this work if I define the BindAttribute for Enquiry class as it is stated here:
How do I use the [Bind(Include="")] attribute on complex nested objects?
Yes, you can make it work like that:
[Bind(Include = "EnquiryId")]
public class Enquiry
{
public int EnquiryId { get; set; }
public string Latitude { get; set; }
}
and your action:
[ActionName("Foo"), HttpPost]
public ActionResult Foo_post(FooViewModel foo)
{
return View(foo);
}
This will include only the EnquiryId in the binding and leave the Latitude null.
This being said, using the Bind attribute is not something that I would recommend you. My recommendation is to use view models. Inside those view models you include only the properties that make sense for this particular view.
So simply readapt your view models:
public class FooViewModel
{
public EnquiryViewModel Enquiry { get; set; }
}
public class EnquiryViewModel
{
public int EnquiryId { get; set; }
}
There you go. No longer need to worry about binding.
IMHO there is a better way to do this.
Essentially if you have multiple models in the view model the post controller's signature would contain the same models, as opposed to the view model.
I.E.
public class FooViewModel {
public Bar BarV { get; set; }
public Enquiry EnquiryV { get; set; }
public int ThisNumber { get; set; }
}
public class Bar {
public int BarId { get; set; }
}
public class Enquiry {
public int EnquiryId { get; set; }
public string Latitude { get; set; }
}
And the post action in the controller would look like this.
[ActionName("Foo"), HttpPost]
public ActionResult Foo_post(
[Bind(Include = "EnquiryId")]
Enquiry EnquiryV,
[Bind(Include = "BarId"])]
Bar BarV,
int ThisNumber
{
return View(new FooViewModel { Bar = BarV, Enquiry = EnquiryV, ThisNumber = ThisNumber });
}
All while the view still looks like this
#using (Html.BeginForm()) {
#Html.TextBoxFor(m => m.EnquiryV.EnquiryId)
#Html.TextBoxFor(m => m.EnquiryV.Latitude)
#Html.TextBoxFor(m => m.BarV.BarId)
#Html.TextBoxFor(m => m.ThisNumber)
<input type="submit" value="push" />
}
Keep in mind, this form will still post Latitude back (the way you had it set up), however since it is not included in the Bind Include string for Enquiry on the post action, the action will not accept the new value in the resultant Enquiry. I'd suggest making latitude either disabled or not a form element to prevent additional posting data.
In any other scenario you can use bind just fine, but for some reason it dislikes the dot notation for complex models.
As a side note, I wouldn't put the bind attribute on the class directly as it can cause other issues like code replication, and doesn't account for certain scenarios where you may want to have a different binding.
(I modified the variable names for some clarity. I am also aware your question is rather dated, however in searching for the answer myself this is the first SO I stumbled upon before trying my own solutions and coming to the one I posted. I hope it can help out other people seeking a solution to the same issue.)