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.
Related
In my MVC application, I normally have ViewModel for such actions as Edit, mainly for populating selection lists for the properties to modify. For example my Model looks like this:
class MyModel{
public int? NavPropID {get; set;}
public AnotherModel NavProp {get; set;}
...
}
Now I was wondering, how should I design my ViewModel to reference the NavProp.
class MyViewModel{
public int? NavPropID {get; set;}
public AnotherModel NavProp {get; set;}
...
}
Should I use both NavPropID and NavProp, or should I use only one (which?) in the ViewModel?
I wanted to use only NavProp at first, because it feels more natural to me (I feel it hides the database implementation detail and it is how I do in WPF and MVVM). But then in my Edit view, I have something like this:
#Html.DropDownListFor(model => model.NavProp,
new SelectList(Model.AllAnotherModels.Select(
i => new {Item = i, Text = i.Name}), "Item","Text"))
In the postback of Edit action, I can see that NavProp is not correctly bonded by the binder because the attemptedValue is of type string and the value is the class name of NavProp (which I guess means it uses ToString() method to post back to the controller). How can I make it work for the post back action?
I then tried to only have NavPropID in the ViewModel. But there are two problems: 1) it means I have to load the actual NavProp in the controller before I use AutoMapper to map back from ViewModel to Model. I feel that this is beyond the responsibility of a controller and 2) even if I load the actual property in the controller, I have some problem later when I am updating by calling DBSet.Attach().
So what is the best practice for the ViewModel to reference a navigational property in the Model?
UPDATE:
Here is my post back function of the Edit action. It is generic I think so I didn't paste it in the first place.
[HttpPost]
public ActionResult Edit(MyViewModel vm)
{
if (ModelState.IsValid)
{
... // I use Automapper to map back to Model and then
// use UnitOfWork and Repository to update the Model
}
}
And my ModelState.IsValid is false so it can't proceed. As I mentioned, I then checked the ModelState (a dictionary) and found out that NavProp is invalid and the attemptedValue is the class name while I think it should be the actual value of the property.
In terms of having your NavPropID within the base view model, we need to establish whether it's appropriate to flatten your view model down.
If the view model is for a page that displays a single navigation properties' information, I would be tempted to have the following:
// Domain model
public class MyModel
{
public int? NavPropID { get; set; }
public NavProp NavProp { get; set; }
...
}
// Nav prop domain model
public class NavProp
{
public int? NavPropID { get; set; }
// Another property of your navigation property
public string AnotherProperty { get; set; }
}
// Flat view model containing all your navigation properties' properties
public class MyViewModel
{
public int? NavPropID { get; set; }
public string AnotherProperty { get; set; }
}
If you wanted you could introduce another view model to represent your NavProp item - but it's overkill if you are happy to flatten it:
// Domain model
public class MyModel
{
public int? NavPropID {get; set;}
public NavProp NavProp {get; set;}
...
}
// Nav prop domain model
public class NavProp
{
public int? NavPropID {get; set;}
// Another property of your navigation property
public string AnotherPropery {get; set;}
}
// View model representing your navigation property
public class NavPropViewModel
{
public int? NavPropID { get; set; }
public string AnotherProperty { get; set; }
}
// Main view model
public class MyViewModel
{
// Use another view model that represents your NavProp
public NavPropViewModel NavProp { get; set; }
...
}
I would say that it is definitely the responsibility of the controller to map from the objects received from your business layer to your view models and vice versa.
To make this cleaner, I would introduce DTOs to map to which can then be passed up to your business layer. Check out general comments on this post.
Update
Here's how you would create your form to post back to MyViewModel with a NavPropViewModel field:
Example View:
#model MyViewModel
#using (Html.BeginForm())
{
#Html.LabelFor(x => x.NavProp.AnotherProperty)
#Html.TextBoxFor(x => x.NavProp.AnotherProperty)
}
Example controller:
[HttpPost]
public ActionResult Edit(MyViewModel vm)
{
// Get the posted value of AnotherProperty
var postedValue = vm.NavProp.AnotherProperty;
}
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'm fairly new to ASP.NET MVC4 and I have a search/filter form where you can filter on multiple parameters
So this is my controller
public ActionResult Index(string page, int? neighborhoodID, int? accommodationType) {
...
}
I was thinking. I'm using data annotations and validation for my login/registering by using the Model class.
Is there a way I could filter values using the Model class?
Now I just look at the requested parameters and use them in my linq query to get the filtered records.
I think you should create an IndexViewModel class
public class IndexViewModel
{
public int? NeighbourhoodId { get; set; }
public int? AccomodationType { get; set; }
}
Then add #model IndexViewModel to the top of your view
It seems that neigbourhoodId and accomodationType come from dropdowns, so map viewModel properties to those dropdowns
and then the controller method will be somehting like this:
public ActionResult Index(string page, IndexViewModel model)
{
// You can use model.NeighbourhoodId and model.AccomodationType the same way you did with parameters
}
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...
I have a master view model, with the following two lists that are from two different DB tables,
public IEnumerable <Customer> SomeCustomer { get; set; }
public IEnumerable <CustomerSite> CustomerSites { get; set; }
In my controller I have
public ViewResult Index()
{
masterViewModel sitesModel = new masterViewModel();
return View(sitesModel);
}
Then in my view I write
#model IEnumerable<trsDatabase.Models.masterViewModel>
If I try to access the model by using a for each statement
#foreach (var customer in Model)
When I type #customer. It will only let me access the two lists not the individual properties in the list.
I thought I would have been able to do this with e.g.
#customer.CustomerSites.UnitNo
But the furthest i can go is
#customer.CustomerSites
And this obviously will not let me access the individual properties in the lists
Does anyone have any ideas on what I'm doing wrong here? Do I need to define the properties from the viewmodel in the controller? (I think not)
Update
I have the following code in my viewmodel,
namespace trsDatabase.Models
{
public class masterViewModel
{
public IEnumerable <Customer> Customers { get; set; }
public IEnumerable <CustomerSite> CustomerSites { get; set; }
}
}
Then the following in the controller,
public ViewResult Index()
{
masterViewModel sitesModel = new masterViewModel();
return View(sitesModel);
}
Then I changed my view declaration to
#model trsDatabase.Models.masterViewModel
This allows me to access the properties from the view perfectly, but it throws an error with the foreach loop as it can't be used when the view is inheriting the model rather than the Ienumerable list, do I need to change the syntax for rendering the list in the view and use something other than a foreachloop?
I think you need another loop, as CustomerSites is a collection:
#foreach (var customer in Model)
foreach (var site in customer.CustomerSites)
#site.UnitNo
EDIT AFTER COMMENT
Create a ViewModel class:
class ViewModel
{
public IEnumerable<Customer> Customer { get; set; }
}
Populate it in the controller and pass it to View() function.
Change the view to be typed (#model ViewModel).
Then inside view your Model property will be of type ViewModel and you can access customers collection through #Model.Customers