ASP.NET MVC - post multiple complex objects with form - asp.net-mvc

In an ASP.NET MVC application I am trying to submit multiple objects with a single form. I am able to get simple types to post back but am having issues with complex types. I feel like I have mimicked the example provided by Phil Haack in his blog post Model Binding To A List but with no luck. Even going so far as copying his code exactly to no avail.
I am trying to populate the ProjectNum and TaskNum properties of a set of MachineLicenseBillback objects. Unfortunately the IList<MachineLicenseBillback> machinePts always ends up as a null when posted.
What am I missing?
Class
public class MachineLicenseBillback
{
public MachineLicenseBillback() { }
public virtual int MachineId { get; set; }
public virtual string ProjectNum { get; set; }
public virtual string TaskNum { get; set; }
public virtual string VerifiedFlag { get; set; }
public virtual DateTime? RcdChgDateTime { get; set; }
public virtual string RcdChgAgent { get; set; }
}
Action
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult TrueUp(int id, IList<MachineLicenseBillback> machinePts)
{
// ...
}
Form
<% using (Html.BeginForm("TrueUp", "Home", new { id = Model.Customer.Id },
FormMethod.Post))
{ %>
<input type="hidden" name="machinePts.Index" value="<%= machine.MachineId %>" />
<input type="text" name="machinePts[<%= machine.MachineId%>].ProjectNum"
value="<%= machine.MachineLicenseBillback.ProjectNum %>" />
<input type="text" name="machinePts[<%= machine.MachineId %>].TaskNum"
value="<%= machine.MachineLicenseBillback.TaskNum %>" />
<input type="submit" value="Submit" />
<% } %>

The .Index syntax was removed for MVC 1 RTM and reintroduced in MVC 2. For MVC 1, list elements must be numbered sequentially: machinePts[0], machinePts[1], etc.

Scott Hanselman has a complete walkthrough here for binding lists of items. In short, your controller method needs an array of MachineLicenseBillback, not an IList.
public ActionResult TrueUp(int id, MachineLicenseBillback[] machinePts)
{
// ...
}
Looking at your code, if you want to bind to an IDictionary (not an IList), you can use key/value pairs in the view instead. Or can keep the code you currently have in the view, and use an Array as the parameter in the controller method.
Pay special attention to the naming conventions. If there is a mismatch in naming, the model binder won't pick up the data.

Related

Sending up a list of objects on a create form

I have a view specific model that combines a few of my objects together but I was having a few issues with a list of objects.
My model is so:
public class RouteSubcontract
{
public RoutingSubcontracts Subcontracts { get; set; }
public RoutingPhases Phases { get; set; }
public List<RoutingApprovals> Approvals { get; set; }
}
I have my create form and everything is working correctly, I am using html helpers like so:
#Html.EditorFor(model => model.Subcontracts.subRevNbr, new { htmlAttributes = new { #class = "textReplace", #id = "frmRevNbr" } })
But the problem is when I need to have the list of approvals, I am not even sure how to start with this. I need the ability to create a list of items, I can with jquery have a button that then creates the row of textboxes I need to enter the data, but I am unsure how to ID or name them so that they are picked up correctly by my post back.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AdminRoutingCreate(RouteSubcontract rs)
For collections the modelbinder expects inputs named in the following format: ListProperty[Index].Property. For example, if you wanted to edit a property on RoutingApprovals named Foo, you're need something like:
<input type="text" name="Approvals[0].Foo" />
<input type="text" name="Approvals[1].Foo" />
<input type="text" name="Approvals[2].Foo" />
On post back you'd end up with three RoutingApprovals instances.

Postback a single item on a Razor ListBoxFor

I have a ListBoxFor helper that is populated with users that have to be verified from a db. I click a single user and then click a submit button. This works fine and sets the admin verified bit in the db to true.
However what I am trying to do is on an item in the list being clicked, a value auto posted back and then I will fill a textarea with the users description. I gather I will use AJAX but have found it hard to get good documentation on using AJAX with HTMLHelpers in the this way.
EDIT: Updated the Model, View and Controller as per suggestions.
Model:
public class UserAdminVerifyModel
{
public SelectList ToBeVerifiedAdmin { get; set; }
public string[] SelectedUsers { get; set; }
public List<string> userdesc { get; set; }
}
Controller:
public ActionResult AdminVerifyListBox()
{
UserAdminVerifyModel verifusermodel = new UserAdminVerifyModel();
verifusermodel.ToBeVerifiedAdmin = GetUsersToBeVerified();
return View(verifusermodel);
}
View:
#using (Html.BeginForm("AdminVerifyListbox", "UserRegLog"))
{
#Html.ListBoxFor(x => x.SelectedUsers, Model.ToBeVerifiedAdmin)
<br />
<input type="submit" value="Submit" title="submit" />
}
}
ListBoxFor is used to generate a multiple selection list. This means that you should not be binding it to a simple string property. You should use an array of strings:
public class UserAdminVerifyModel
{
public SelectList ToBeVerifiedAdmin { get; set; }
public string[] SelectedUsers { get; set; }
public List<string> userdesc { get; set; }
}
and in the view bind the ListBoxFor to the SelectedUsers collection property:
#using (Html.BeginForm("AdminVerifyListbox", "UserRegLog"))
{
#Html.ListBoxFor(x => x.SelectedUsers, Model.ToBeVerifiedAdmin)
<br />
<input type="submit" value="Submit" title="submit" />
}
Also your ToBeVerifiedAdmin is already a SelectList. You should not be calling the constructor once again in your view. This should be done in your controller action which is responsible for populating this ToBeVerifiedAdmin property from wherever your information is stored.

Mapping a list of string key-value pairs with Razor / form post

I have some HTML in my Razor view that is basically 3 rows of a pair of Select and Input elements:
(There could be more than 3)
<select name="SocialNetwork[1]">
<option>Skype</option>
<option>Twitter</option>
<option>Facebook</option>
</select>
<input name="SocialNetworkUsername[1]" type="text" />
<select name="SocialNetwork[2]">
<option>Skype</option>
<option>Twitter</option>
<option>Facebook</option>
</select>
<input name="SocialNetworkUsername[2]" type="text" />
<select name="SocialNetwork[3]">
<option>Skype</option>
<option>Twitter</option>
<option>Facebook</option>
</select>
<input name="SocialNetworkUsername[3]" type="text" />
When the form is posted to my controller method:
public ActionResult SaveDetails(MyModel model)
{
}
How can I 'map' the values of SocialNetwork | SocialNetworkUsername to a model?
Something like:
public class MyModel
{
public string SomeProperty { get; set; }
public string SomeOtherProperty { get; set; }
public Dictionary<string,string> SocialNetworks { get; set; }
}
I'm not 'stuck' using a dictionary if there is something more appropriate
You can use an editor template to achieve this.
the editor template will allow you to loop through a list of social networks in your main view and use the #Html.EditorFor helper to render out your dropdown lists and make model binding easy.
To start with your model should represent a single Social Network
In your model you need properties to contain the dropdown list values, and a property for the selected value. You'll also need a property to contain the SocialNetworkUsername
public IEnumerable<SelectListItem> SocialNetworks { get; set; }
public string SelectedSocialNetwork { get; set; }
public string SocialNetworkUserName { get; set; }
To make your editor template add a new folder /Views/Shared/EditorTemplates
In the EditorTemplates folder create a new view with the name MyClass (or whatever your model class name is)
You need to follow these naming conventions as there is no other configuration required for the editor template to work.
In MyClass.cshtml you need to allow editing of a single social network
#model MyModel
#Html.DropDownListFor(m => m.SelectedSocialNetwork, Model.SocialNetworks)
#Html.TextBoxFor(m => m.SocialNetworkUserName)
Then in your main view you use a List of MyModel as the model for the view and use a for loop to render out your editor templates.
#model List<MyModel>
#using(Html.BeginForm())
{
for (var i = 0; i < Model.Count; i++)
{
Html.EditorFor(Model[i])
}
}
In your controller just change your action method parameter to accept a List of MyClass and model binding should just magically work ;)

MVC3 using CheckBox with a complex viewmodel

Right guys. I need your brains as I can't find a way to do this properly.
I have a view model:
public class EditUserViewModel
{
public User User;
public IQueryable<ServiceLicense> ServiceLicenses;
}
User is unimportant as I know how to deal with it.
ServiceLicenses has the following implementation:
public class ServiceLicense
{
public Guid ServiceId { get; set; }
public string ServiceName { get; set; }
public bool GotLic { get; set; }
}
Getting a checked list of users is cool. It works like a charm.
<fieldset>
<legend>Licenses</legend>
#foreach (var service in Model.ServiceLicenses)
{
<p>
#Html.CheckBoxFor(x => service.GotLic)
#service.ServiceName
</p>
}
</fieldset>
The problem I'm having is getting the updated ServiceLicenses object with new checked services back to the HttpPost in my controller. For simplicity lets say it looks like this:
[HttpPost]
public ActionResult EditUser(Guid id, FormCollection collection)
{
var userModel = new EditUserViewModel(id);
if (TryUpdateModel(userModel))
{
//This is fine and I know what to do with this
var editUser = userModel.User;
//This does not update
var serviceLicenses = userModel.ServiceLicenses;
return RedirectToAction("Details", new { id = editUser.ClientId });
}
else
{
return View(userModel);
}
}
I know I am using CheckBox the wrong way. What do I need to change to get serviceLicenses to update with the boxes checked in the form?
i understand that ServiceLicenses property is a collection and you want MVC binder to bind it to you action parameters property. for that you should have indices attached with inputs in your view e.g
<input type="checkbox" name = "ServiceLicenses[0].GotLic" value="true"/>
<input type="checkbox" name = "ServiceLicenses[1].GotLic" value="true"/>
<input type="checkbox" name = "ServiceLicenses[2].GotLic" value="true"/>
Prefix may not be mandatory but it is very handy when binding collection property of action method parameter. for that purpose i would suggest using for loop instead of foreach and using Html.CheckBox helper instead of Html.CheckBoxFor
<fieldset>
<legend>Licenses</legend>
#for (int i=0;i<Model.ServiceLicenses.Count;i++)
{
<p>
#Html.CheckBox("ServiceLicenses["+i+"].GotLic",ServiceLicenses[i].GotLic)
#Html.CheckBox("ServiceLicenses["+i+"].ServiceName",ServiceLicenses[i].ServiceName)//you would want to bind name of service in case model is invalid you can pass on same model to view
#service.ServiceName
</p>
}
</fieldset>
Not using strongly typed helper is just a personal preference here. if you do not want to index your inputs like this you can also have a look at this great post by steve senderson
Edit: i have blogged about creating master detail form on asp.net mvc3 which is relevant in case of list binding as well.

ASP.NET MVC 3 How to have multi-field create capability for Model with ICollection Property on Create view

Note: I'm using MVC3+Razor, EF4, CF-CTP5
How can you allow the view to have the ability to add multiple Address classes per Organization dynamically on the client, and bound strongly to the model on post?
How can you have the view parse values in the model if the (ModelState.IsValid == false) such that if you enter 3 addresses and post an invalid model, it re-populates the number addresses and with their appropriate values?
Here are my models:
public class Organization
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
...
}
public class Address
{
public int Id { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public int Type { get; set; }
}
I'm trying to figure out how you can have the Create action for Organization (/Organization/Create) handle the create like thus (such that addresses and phone numbers are part of the submitted model):
[HttpPost]
public ActionResult Create(Organization organization)
{
if (ModelState.IsValid)
{
_db.Organizations.Add(organization);
_db.SaveChanges();
return RedirectToAction("Details", organization.Id);
}
return View(organization);
}
Your question is quite vaste :)
This is just one of the way your requirement can be achieved and I am sure there are better than mine.
I am going to start from your second question:
How can you have the view parse values
in the model if the
(ModelState.IsValid == false) such
that if you enter 3 addresses and post
an invalid model, it re-populates the
number addresses and with their
appropriate values?
If I correctly understand your request it looks very simple to me. The answer is simply code your view to render a Model class content and return the invalid model to the client exactly as you are doing in your Create action.
If your form (and its fields) have been decorated with the ValidationSummary/ValidationMessage html helpers, you are going to see also validation messages.
How can you allow the view to have the ability to add multiple Address
classes per Organization dynamically
on the client, and bound strongly to
the model on post?
You can have a main view showing Organization attributes and then have another view showing related addresses. Here you can place a hyperlink or a button that open a dialog for adding a new address object and then refresh the address list when done. At the same way you can have edit and delete buttons as icons on the list.
The address list is a piece of markup completely handled at client side that, to be correctly binded to the server side Model class should adhere to some simple naming rules for it's input attributes.
To make the Default Model Binder class bind correctly your form use the following snippet for your Organization class
#using (Html.BeginForm()) {
#Html.HiddenFor(o => o.Id)
#Html.ValidationSummary( true )
<fieldset>
<legend>My Organization</legend>
<div class="editor-label">#Html.LabelFor( model => model.Name )</div>
<div class="editor-field">
#Html.EditorFor( model => model.Name )
#Html.ValidationMessageFor( model => model.Name )
</div>
<br />
<div id="container">
<div>Address List</div>
#foreach (Address a in Model.Addresses ) {
Html.EditorFor(a);
}
</div>
<div style="text-align:right;margin-top:14px;">
<input type="submit" id="btnSubmit" value="Save" />
</div>
</fieldset>
}
To be automatically bindable the resultant code for the form should look as the following
<form action="..." id="..." method="post">
<input type="hidden" name="Id" value="2">
<input type="hidden" name="Name" value="Acme Corporation">
<!-- markup for each address -->
<input type="hidden" name="Addresses[0].Id" value="1">
<input type="hidden" name="Addresses[0].Line1" value="Line 1">
<input type="hidden" name="Addresses[0].Line2" value="Line 2">
... and so on...
</form>
having it's properties named as Addresses[index].PropertyName.
If you add new addresses on the client it does'nt matter so much: as long as your code respect this rule you can have the default Model Binder do the job for you.
Hope this helps
I'm not sure if I understand your question correctly but with respect to question 1 I think you are looking for a ViewModel. Like this perhaps..
OrganizationViewModel.cs
public class OrganizationViewModel
{
public OrganizationViewModel(Organization org, IList<Address> addresses)
{
this.Organization = org;
this.Addresses = addresses
}
public Organization Organization {get;set;}
public IList<Address> Addresses {get;set;}
}
OrganizationController.cs
public class OrganizationController : Controller
{
private readonly IOrganizationService _organizationService: //or whatever method you use
public OrganizationController(IOrganizationService orgService)
{
this._organizationService = orgService;
}
public ActionResult Item(int id)
{
var org = _organizationService.GetOrganizationById(id);
var addresses = _organizationService.GetOrgAddressesByOrgId(id);
return View(new OrganizationViewModel(program, addresses));
}
}
Item.cshtml
#model OrganizationViewModel
<h1>#Model.Organization.Name</h1>
<ul>
#foreach(var a in Model.Addresses)
{
<li>#a.Line1</li>
<li>#a.Line2</li>}
</ul>
Before I try and answer number 2 maybe you should indicate whether I am correctly understanding question 1. Hope this helps.
I managed to do this using LINQ to SQL. Now I'm trying to use Entity Framework instead, but it really makes everything more complicated. So I don't have a solution for you, but perhaps my L2S solution might help?
Using models generated from my database I could in my view do this:
#for (int i = 0; i < Model.Contact.EmailAddresses.Count; ++i)
{
<li>
#Html.TextBoxFor(x => x.Contact.EmailAddresses[i].EmailAddress)
#Html.HiddenFor(x => x.Contact.EmailAddresses[i].EmailAddressID)
</li>
}
I had a view model class:
class ContactViewModel
{
Contact contact { get; set; }
}
This worked fine and in my controller action I got my Contact object with it's Contact.ContactEmailAddresses list filled just like I expected.
But with EF, I cannot use the [i] on the EmailAddresses property generated from the database anymore. The best I have come up with is:
#Html.TextBox("Contact.EmailAddresses[" + i + "].EmailAddress", Model.Contact.EmailAddresses.ElementAt(i).EmailAddress)

Resources