Passing a page view model to a form - asp.net-mvc

I have a .net MVC page that has a form used to edit an entity from database. The form has some dynamic select list fields that need to be populated from Database also.
I pass a PageView Model that contains the Data for the entity, and a list of entities to populate the select list like so:
public class EditPortalPage
{
public PortalViewModel Data { get; set; }
public IEnumerable<SelectListItem> Cultures { get; set; }
}
in my View I have
#model Website.Models.Portals.EditPortalPage
at the top of the page
I can easily populate my form doing this:
#Html.EditorFor(model => model.Data.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.LabelFor(model => model.Data.Name)
#Html.ValidationMessageFor(model => model.Data.Name, "", new { #class = "text-danger" })
#Html.DropDownListFor(model => model.Data.DefaultCultureId, new SelectList(Model.Cultures, "Value", "Text"), new { #class = "mdb-select colorful-select dropdown-info" })
#Html.LabelFor(model => model.Data.DefaultCultureId)
#Html.ValidationMessageFor(model => model.Data.DefaultCultureId, "", new { #class = "text-danger" })
The problem is this produces messed up Id and Name attributes for my form fields. In the above example it produces:
Name Field ID: Data_Name
Name Field Name: Data.Name
What is the correct way to set it up so I the name and ID does not include the 'Data' prefix
Can I pass the Data model directly to teh form?

The HtmlHelper methods generate the name and id attribute based on the name of the property. In your case, its a nested property so it generates the Data. prefix for the name attribute, and in the case of the id attribute, it replaces the .(dot) with an _(underscore) so that the id can be used in jquery selectors (otherwise the dot and the following text would be interpreted as a class selector.
Basing the id attribute on the property name ensures a unique id (and therefore valid html), and I'm not sure why you think its 'messed up', but you can always override the value by using
#Html.EditorFor(m => m.Data.Name, new { htmlAttributes = new { id = "xxx", #class = "form-control" } })
which will render <input id="xxx" .... /> if you want to use xxx in a css or javascript/jquery selector (which is the only purpose of the id attribute)
However the name attribute is certainly not 'messed up' and if it were changed, it would not bind to your model when you submit. Under no circumstance should you ever attempt to change the name attribute when using the HtmlHelper methods.
I'm guessing that your (incorrectly) using your data model as a parameter in the POST method, in which case you could use the Prefix property of the [Bind] attribute (although a [Bind] attribute should never be used with a view model)
public ActionResult Edit([Bind(Prefix = "Data")]PortalViewModel model)
to strip the "Data." prefix and bind to PortalViewModel.
However, the real issue here is you incorrect use of a view model. View models, especially when editing data, should not contain data models. They contain only the properties of your data model that you need in the view. Some of the benefits of view models include Separation of Concerns, protection against under and over posting attacks, ability to add display attributes which are not applicable in data models, and the ability to add additional view specific properties. In your case, you have negated all but the last point. You view model should be
public class EditPortalPage
{
public string Name { get; set; }
[Display(Name = "Culture")]
[Required(ErrorMessage = "Please select a culture")]
public int? SelectedCulture { get; set; } //nullable and Required to protect against under-posting attacks
... // other properties of PortalViewModel that you need in the view
public IEnumerable<SelectListItem> Cultures { get; set; }
}
and in the POST method, you map your view model to the data model and save the data model. Refer also What is ViewModel in MVC?.
As a side note, using new SelctList(..) in your DropDownList() method is pointless extra overhead - its just creating another identical IEnumerable<SelectListItem> from the first one, and your code should just be
#Html.DropDownListFor(m => m.SelectedCulture, Model.Cultures, "Please select", new { ... })

If I understand the Q correctly, you could use a partial "_data.cshtml":
#model PortalViewModel
#Html.EditorFor(model => model.Name, new { htmlAttributes = new { #class = "form-control" } })
Pass data to the partial from your view:
#Html.Partial("_data",model.Data)

Related

Issue with Model Binding

I have created a View Model called CompetitionRoundModel which is partially produced below:
public class CompetitionRoundModel
{
public IEnumerable<SelectListItem> CategoryValues
{
get
{
return Enumerable
.Range(0, Categories.Count())
.Select(x => new SelectListItem
{
Value = Categories.ElementAt(x).Id.ToString(),
Text = Categories.ElementAt(x).Name
});
}
}
[Display(Name = "Category")]
public int CategoryId { get; set; }
public IEnumerable<Category> Categories { get; set; }
// Other parameters
}
I have structured the model this way because I need to populate a dropdown based on the value stored in CategoryValues. So for my view I have:
#using (Html.BeginForm())
{
<div class="form-group">
#Html.LabelFor(model => model.CategoryId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.CategoryId, Model.CategoryValues, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.CategoryId, "", new { #class = "text-danger" })
</div>
</div>
// Other code goes here
}
I have selected model.CategoryId in the DropDownListFor() method since I want to bind the selected value to CategoryId. I really don't care for CategoryValues, I just need it to populate the DropDown.
My problem now is that when my Controller receives the values for my Model in the action method, CategoryValues is null which causes the system to throw a ArgumentNullException (the line that is highlighted is the return Enumerable line.
I have even tried [Bind(Exclude="CategoryValues")] but no change at all. Any help would be much appreciated.
Your not (and should not be) creating form controls for each property of each Category in your IEnumerable<Category> collection so in your POST method, the value of Categories is null (it never gets initialized). As soon as you attempt CategoryValues and exception is thrown by your .Range(0, Categories.Count()) line of code in the getter.
Change you view model to give CategoryValues a simple geter/setter, and delete the Categories property
public class CompetitionRoundModel
{
public IEnumerable<SelectListItem> CategoryValues { get; set; }
[Display(Name = "Category")]
public int CategoryId { get; set; }
.... // Other properties
}
and populate the SelectList in the controller methods, for example
var categories db.Categories; // your database call
CompetitionRoundModel model = new CompetitionRoundModel()
{
CategoryValues = categories.Select(x => new SelectListItem()
{
Value = x.Id.ToString(),
Text = x.Name
},
....
};
return View(model);
or alternatively
CompetitionRoundModel model = new CompetitionRoundModel()
{
CategoryValues = new SelectList(categories, "Id", "Name" ),
Note also that if you return the view (because ModelState is invalid, the you need to repopulate the value of CategoryValues (refer The ViewData item that has the key 'XXX' is of type 'System.Int32' but must be of type 'IEnumerable' for more detail)
Since CategoryValues just populates the drop down, it will never post back to the server and you'll need to rebuild the list from the database before using it in the GET or POST operation. The CategoryId property is the value that will be posted back to the server from the DropDownList.

How do I carry a complex object model through a POST request

I have the following entity models:
public class AssetLabel
{
public string QRCode { get; set; }
public string asset { get; set; }
public virtual IEnumerable<Conversation> Conversations { get; set; }
}
public class Conversation
{
public int ID { get; set; }
public virtual AssetLabel AssetLabel{ get; set; }
public string FinderName { get; set; }
public string FinderMobile { get; set; }
public string FinderEmail { get; set; }
public ConversationStatus Status{ get; set; }
public IEnumerable<ConversationMessage> Messages { get; set; }
}
public class ConversationMessage
{
public int ID { get; set; }
public DateTime MessageDateTime { get; set; }
public bool IsFinderMessage { get; set; }
public virtual Conversation Conversation { get; set; }
}
public enum ConversationStatus { open, closed };
public class FinderViewModel : Conversation
{/*used in Controllers->Found*/
}
My MVC application will prompt for a QRCode on a POST request. I then validate this code exists in the database AssetLabel and some other server-side logic is satisfied. I then need to request the user contact details to create a new Conversation record.
Currently I have a GET to a controller action which returns the first form to capture the Code. If this is valid then I create a new FinderViewModel, populate the AssetLabel with the object for the QRCode and return a view to consume the vm and show the fields for the Name, Mobile and Email.
My problem is that although the AssetLabel is being passed to the view as part of the FinderViewModel and I can display fields from the AssetLabel; graphed object the AssetLabel does not get passed back in the POST. I know I could modify the FinderViewModel so that it takes the Conversation as one property and set up the QRCode as a separate property that could be a hidden field in the form and then re-find the the AssetLabel as part of the processing of the second form but this feels like a lot of work seeing as I have already validated it once to get to the point of creating the second form (this is why I am moving away from PHP MVC frameworks).
The first question is HOW?, The second question is am I approaching this design pattern in the wrong way. Is there a more .NETty way to persist the data through multiple forms? At this point in my learning I don't really want to store the information in a cookie or use ajax.
For reference the rest of the code for the 1st form POST, 2nd view and 2nd form POST are shown below (simplified to eliminate irrelevant logic).
public class FoundController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: Found
public ActionResult Index()
{
AssetLabel lbl = new AssetLabel();
return View(lbl);
}
[HttpPost]
public ActionResult Index(string QRCode)
{
if (QRCode=="")
{
return Content("no value entered");
}
else
{
/*check to see if code is in database*/
AssetLabel lbl = db.AssetLables.FirstOrDefault(q =>q.QRCode==QRCode);
if (lbl != null)
{
var vm = new FinderViewModel();
vm.AssetLabel = lbl;
vm.Status = ConversationStatus.open;
return View("FinderDetails", vm);
}
else
{/*Label ID is not in the database*/
return Content("Label Not Found");
}
}
}
[HttpPost]
public ActionResult ProcessFinder(FinderViewModel vm)
{
/*
THIS IS WHERE I AM STUCK! - vm.AssetLabel == NULL even though it
was passed to the view with a fully populated object
*/
return Content(vm.AssetLabel.QRCode.ToString());
//return Content("Finder Details posted!");
}
FinderView.cshtml
#model GMSB.ViewModels.FinderViewModel
#{
ViewBag.Title = "TEST FINDER";
}
<h2>FinderDetails</h2>
#using (Html.BeginForm("ProcessFinder","Found",FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Finder Details</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ID)
#Html.HiddenFor(model => model.AssetLabel)
<div class="form-group">
#Html.LabelFor(model => model.FinderName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FinderMobile, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderMobile, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderMobile, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FinderEmail, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderEmail, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderEmail, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
Rendered HTML snippet for AssetLabel
<input id="AssetLabel" name="AssetLabel" type="hidden"
value="System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3"
/>
You cannot use #Html.HiddenFor() to generate a hidden output for a complex object. Internally the method use .ToString() to generate the value (in you case the output is System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3 which cannot be bound back to a complex object)
You could generate a form control for each property of the AssetLabel - but that would be unrealistic in your case because AssetLabel contains a property with is a collection of Conversation which in turn contains a collection of ConversationMessage so you would need nested for loops to generate an input for each property of Conversation and ConversationMessage.
But sending a whole lot of extra data to the client and then sending it all back again unchanged degrades performance, exposes unnecessary details about your data and data structure to malicious users, and malicious users could change the data).
The FinderViewModel should just contain a property for QRCode (or the ID property of AssetLabel) and in the view
#Html.HiddenFor(m => m.QRCode)
Then in the POST method, if you need the AssetLabel, get it again from the repository just as your doing it in the GET method (although its unclear why you need to AssetLabel in the POST method).
As a side note, a view model should only contain properties that are needed in the view, and not contain properties which are data models (in in your case inherit from a data model) when editing data. Refer What is ViewModel in MVC?. Based on your view, it should contain 4 properties FinderName, FinderMobile, FinderEmail and QRCode (and int? ID if you want to use it for editing existing objects).
Thanks Stephen. The QRCode is the PK on AssetLabel and the FK in Conversation so it needs to be tracked through the workflow. I was trying to keep the viewModel generic so that is can be used for other forms rather than tightly coupling it to this specific form and I was trying to pass the AssetLabel around as I have already done a significant amount of validation on it's state which I didn't want to repeat. I worked out what I need to do - If you use #Html.Hidden(model => model.AssetLabel.QRCode) then the form field name becomes AssetLabel_QRCode and is automatically mapped to the correct place in the POST viewmodel. To promote code reuse and avoid any rework later I have created this logic in a display template with the fields defined as hidden and then #Html.Partial() using the overload method that allows me to define the model extension to the form names
#Html.Partial
(
"./Templates/Assetlabel_hidden",
(GMSB.Models.AssetLabel)(Model.AssetLabel),
new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "AssetLabel"
}
}
)
But you are absolutely right, this exposes additional fields and my application structure. I think I will redraft the viewModel to only expose the necessary fields and move the AssetLabel validation to a separate private function that can be called from both the initial POST and the subsequent post. This does mean extra code in the controller as the flat vm fields need to be manually mappped to the complex object graph.

DropDownList cant get selected values

I'm stucked at creating dropdownlist in ASP.NET MVC.
ViewModel:
public MultiSelectList users { get; set; }
I set the values in controller:
var allUsers = db.Users.Select(u => new {
id = u.UserId,
name = u.Name
}).ToList();
model.users = new MultiSelectList(allUsers, "id", "name");
so selectbox values are set.
In view:
#Html.DropDownListFor(m => m.users, Model.users, new { #class = "form-control" })
The problem is that if I select the value and click submit i get this error:
No parameterless constructor defined for this object.
I think the problem is in the way how I create the dropdownlist in view, I'm not sure how to set it, thanks.
EDIT: If I dont choose any user from dropdown all goes well, but if I choose then the error appears.
You're trying to post to the MultiSelectList property. That's not going to work regardless, but the specific error is related to the fact that MultiSelectList has no parameterless constructor, and there's no way for the modelbinder to new up a class with parameters. Anything involved in the modelbinding process must have a parameterless constructor.
What you should be doing is have an additional property like:
public List<int> SelectedUserIds { get; set; }
And, then bind to that in your view:
#Html.ListBoxFor(m => m.SelectedUserIds, Model.Users)
Also, as you'll notice, I changed DropDownListFor to ListBoxFor. If you're wanting to have a select multiple, you need ListBoxFor.
Looks like it is failing when trying to bind, so to prevent it from binding:
[HttpPost]
public ActionResult YourMethod([Binding(Exclude = "users")] SomeViewModel model)
The post back should go to an IEnumerable to capture the selected items.
Add to view model
public IEnumerable UserList { get; set; }
Change view to
#Html.DropDownListFor(m => m.UserList, Model.users, new { #class = "form-control" })
If you want get selected user id from a dropdownlist you must add a property to your model
public MultiSelectList users { get; set; }
public int SelectedUser { get;set;}
And in view
#Html.DropDownListFor(m => m.SelectedUser, Model.users, new { #class = "form-control" })

MVC MultiSelectList Binding

I use ASP.NET MVC .. When i post my form it's raise cast error when my model validate. How can fixed my view model or another validation way?
"The parameter conversion from type 'System.String' to type 'System.Web.Mvc.SelectListItem' failed because no type converter can convert between these types."
Thank you..
//my view model
public class ProdGroupViewModel
{
//I've to fixed here or another way?
public IEnumerable<SelectListItem> Rooms { get; set; }
}
//controller
public ActionResult Create(int id)
{
return View(new ProdGroupViewModel
{
Rooms = new MultiSelectList(_roomService.GetAll(), "RoomId", "RoomName"),
});
}
//in my view
<div class="form-group">
<label class="col-md-3 control-label">Oda</label>
<div class="col-md-9">
#Html.ListBoxFor(model => model.Rooms, (MultiSelectList)Model.Rooms, new { #class = "form-control" })
</div>
</div>
You're trying to post to the same property that holds your select list. The posted result of a selections in a listbox will be a comma-delimited string of the selected option values, which the modelbinder would be incapable of binding to a property of type MultiSelectList.
You need an additional model property to hold the posted value like:
public List<int> SelectedRoomIds { get; set; }
And then in your view:
#Html.ListBoxFor(m => m.SelectedRoomIds, Model.Rooms, new { #class = "form-control" })
Also, you don't need to cast Model.Rooms, since it's already strongly-typed.

ASP.NET MVC, Using a viewmodel with strongly typed helpers

I have a question about setting up a viewmodel when you use the strongly typed helpers (HTML.EditorFor, etc.) and a viewmodel in ASP.NET MVC. I am working with MVC5, but I would imagine my question is also applicable to other versions.
For this example, I'm working with the create of the CRUD process.
In the example, the user enters the name and address of a person and city is selected from a drop down.
Here is the model:
public class Person
{
[Key]
public int PersonID { get; set; }
[ForeignKey("City")]
public int CityID { get; set; }
public string Name {get; set;}
public string address {get; set;}
//Navigational property
public virtual City City { get; set; }
}
Here is the viewmodel:
public class PersonCreateViewModel
{
public SelectList cities {get; set;}
public Person person { get; set; }
}
Here is the Action Method from the controller used to pass back the view for the create page:
public ActionResult Create()
{
CreateViewModel viewmodel = new CreateViewModel();
viewmodel.cities = new SelectList(db.Cities, "CityID", "name");
return View(viewmodel);
}
Here is part of my view:
<div class="form-group">
#Html.LabelFor(model => model.person.name, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.person.name)
#Html.ValidationMessageFor(model => model.person.name)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.person.address, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.person.address)
#Html.ValidationMessageFor(model => model.person.address)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.person.CityID, "CityID", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("cities")
#Html.ValidationMessageFor(model => model.person.CityID)
</div>
</div>
I declare the model for my view as such:
#model TestProjects.ViewModels.PersonCreateViewModel
And Lastly, the http post method in the controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="PersonID,CityID,nameaddress")] Person person)
{
if (ModelState.IsValid)
{
//Add to database here and return
}
//return back to view if invalid db save
return View(person);
}
So at one point I had all of this working. Then I decided I wanted to use the ViewModel approach. I still don't have it working, but here are some questions:
In the view, I reference the properties of the model with model.person.address. Is this the proper way to do this? I noticed that when it generates the html, it names the field person_address, etc.
So should I just change the Bind properties in the http post controller mehtod to reflect this? But if I change this, the properties will no longer match up with the person object causing a disconnect.
Or should I change my view model? And instead of having a person type in my ViewModel, copy/paste all of the fields from the model into the ViewModel? I guess this would also work, but is that the typical way it is done? It seems redundant to list out every property of the model when I could just have an instance if the model in the viewmodel?
In the view, I reference the properties of the model with model.person.address. Is this the proper way to do this? I noticed that when it generates the html, it names the field person_address, etc.
Yes, that is the correct way to reference model properties. More accurately, since model in your helper expressions is a reference to the Func's input parameter, it could be anything. The following would work just as well:
#Html.EditorFor(banana => banana.person.address)
So should I just change the Bind properties in the http post controller mehtod to reflect this? But if I change this, the properties will no longer match up with the person object causing a disconnect.
You don't need the bind parameters at all. What you should do is take all reference to your data entities (i.e. Person) out of your view model completely (otherwise using the view model is a little pointless as it's tightly coupled with your data entities anyway) and give the view model properties that the view needs, e.g.:
public class PersonCreateViewModel
{
public SelectList Cities { get; set; }
public string Address { get; set; }
public string Name { get; set; }
...
}
They should then bind back by default to the same model, presuming your view is correct:
public ActionResult Create (PersonCreateViewModel model)
{
// Map PersonCreateViewModel properties to Person properties
}

Resources