Bind List of objects to a form [duplicate] - asp.net-mvc

This question already has answers here:
Post an HTML Table to ADO.NET DataTable
(2 answers)
Closed 4 years ago.
I have a class of Signature objects:
public class Signature
{
public int SignatureID { get; set; }
public int FormID { get; set; }
public string Title { get; set; }
public string Email { get; set; }
[Display(Name = "Signed Date:")]
public DateTime? Date { get; set; }
}
I have a Form.cs class that has a virtual list of signatures
public virtual List<Signature> Signatures { get; set; }
In my controller, I populate the list by:
form.Signatures = repository.Signatures.Where(s => s.FormID == form.FormID).ToList();
In my Form View, I display a list of the associated signatures:
#foreach (var signature in Model.Signatures)
{
<div class="text-center">
<label asp-for="#signature.Title"></label>
<input asp-for="#signature.Title" />
<label asp-for="#signature.Email"></label>
<input asp-for="#signature.Email" />
<label asp-for="#signature.Date"></label>
<input disabled asp-for="#signature.Date">
</div>
}
However, I don't know how to update the associated signatures upon my POST method of the form. For example, if I change the Email property of a signature and POST the form, the model does not bind this change into the Form object. In this case, form.Signatures is null.
How can I ensure changes to the <List>Signature items associated with the form are updated on POST?

Use the for loop to generate the elements, as it would add indexing to the property names which is used by model binder to bind to List on the post which does not work with the foreach:
#for (int i=0; i< Model.Signatures.Count; i++)
{
<div class="text-center">
<label asp-for="#Model.Signatures[i].Title"></label>
<input asp-for="#Model.Signatures[i].Title" />
<label asp-for="#Model.Signatures[i].Email"></label>
<input asp-for="#Model.Signatures[i].Email" />
<label asp-for="#Model.Signatures[i].Date"></label>
<input disabled asp-for="#Model.Signatures[i].Date">
</div>
}
Now the elements would be rendered with names like Signatures[0].Title, Signatures[1].Title and the model binder can bind it to the model on post.
Hope it helps.

Related

How to use asp-for to get value from a model and set value to another model?

I'm trying to build one view that includes all (Create, Edit, Delete, and Index) in one View which is Index.
The problem is with Editing. Always returns null to the controller as shown in the gif.
I know what is the problem but I can't solve it. The explanation at the end of the question after the hr line
I have Model and ViewModel as follows.
The Model BootstrapCategory
public class BootstrapCategory
{
[Key]
public Guid Id { get; set; }
[MaxLength(20)]
[Required]
public string Category { get; set; }
}
The ViewModel VMBPCategoris
public class VMBPCategoris
{
public List<BootstrapCategory> bootstrapCategories { get; set; }
public Guid Id { get; set; }
public string Category { get; set; }
}
The View
Note: Edit not by the usual button in the table it instead by another
button as shown in the gif
#model VMBPCategoris
#foreach (var item in Model.bootstrapCategories)
{
<tr>
<td>
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="#item.Id" />
<div class="#item.Id d-none">
<div class="input-group">
<input id="btnGroupEdit" type="submit" value="Save" class="input-group-text btn btn-primary" />
<input asp-for="#item.Category" class="form-control" aria-label="Input group example" aria-describedby="btnGroupEdit">
</div>
<span asp-validation-for="#item.Category" class="text-danger"></span>
</div>
</form>
<div class="#item.Id">
#Html.DisplayFor(modelItem => item.Category)
</div>
</td>
<td>
Edit |
<a asp-action="Details" asp-route-id="#item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="#item.Id">Delete</a>
</td>
</tr>
}
The Controller
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit([Bind("Id,Category")] BootstrapCategory bootstrapCategory)
{
_context.Update(bootstrapCategory);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
//return View(vMBPCategoris);
}
In the ViewModel VMBPCategoris
The problem, I get the list of Category values from this list
public List<BootstrapCategory>bootstrapCategories { get; set; }
on edit, I set a new value to one member in the list, not to this
public Guid Id { get; set; }
public string Category { get; set; }
So that It returns Null
However, if I change asp-for="#item.Category" to
asp-for="Category" the input gets empty value, but it returns the edited value not empty
**Here is my question >> can I use asp-for to get value from
public List<BootstrapCategory> bootstrapCategories { get; set; }
and set value to
public Guid Id { get; set; }
public string Category { get; set; }
Because,
using asp-for="#item.Category" is beneficial to get category values but not for setting a value.
And using asp-for="Category" is beneficial to set category value but not for getting a value.
Finally, I found a very simple solution.
to get values I used
value="#item.Category"
and to set a value I used
asp-for="Category"
That's it ...
<input asp-for="Category" value="#item.Category" class="form-control">
In that way, I can get a value from a model and set a value to another model

MVC Core - strange view rendering issue

I am using MVC to display a simple form in a view:
ViewModel:
public class CreateSaleViewModel
{
public string OrderId { get; set; }
public decimal TotalAmount { get; set; }
public bool ShowInstoreConfirmDetails { get; set; }
}
Controller action:
[HttpGet]
public IActionResult CreateSale()
{
return View(new CreateSaleViewModel());
}
View:
#model CreateSaleViewModel
<form asp-controller="Sales" asp-action="CreateSale" method="post">
<input asp-for="OrderId" />
<input asp-for="TotalAmount" />
<button type="submit" name="CreateSale" id="CreateSale">
button
</button>
</form>
I then post to a new view, where the same details need to be entered. To do this I store the old values in hidden inputs and provide another form to re-enter the details.
ViewModel:
public class ConfirmDetailsViewModel
{
public string OrderId { get; set; }
public decimal TotalAmount { get; set; }
public string ConfirmOrderId { get; set; }
public decimal ConfirmTotalAmount { get; set; }
}
Controller:
[HttpPost("Confirmdetails")]
public IActionResult ConfirmDetails(CreateSaleViewModel model)
{
var viewModel = new ConfirmDetailsViewModel
{
ConfirmOrderId = model.OrderId,
ConfirmTotalAmount = model.TotalAmount,
OrderId = string.Empty,
TotalAmount = 0.0m
};
return View("ConfirmDetails", viewModel);
}
View:
#model ConfirmDetailsViewModel
<form asp-controller="Sales" asp-action="Summary" method="post">
<input type="hidden" value="#Model.ConfirmOrderId" id="OrderIdConfirm" />
<input type="hidden" value="#Model.ConfirmTotalAmount" id="TotalAmountConfirm" />
<input type="hidden" value="#Model.OrderId" id="banana" />
<input asp-for="OrderId" />
<input asp-for="TotalAmount" />
<button type="submit" name="CreateSale" id="CreateSale">
button
</button>
</form>
My problem is on the confirmdetails view orderId and TotalAmount retain the values that were posted from the previous page.
I have debugged the controller and can see the ConfirmOrderId and ConfirmTotalAmount properties have the correct values, and also OrderId and TotalAmount are empty strign and 0 respectively.
Even stranger is that
<input type="hidden" value="#Model.OrderId" id="banana" />
Has the correct value of "".
Does anyone know what is causing this issue?
MVC stores the posted back values in ModelState.
These values are used by default in #Html helpers - as a convenience. This allows the values of hidden form fields to be preserved through postbacks, even if they don't have properties in the view-model.
Unfortunately what is usually a convenience turns into a headache, if you try to modify the model's properties within the action. Helpers take their values from ModelState, ignoring the updated view-model.
To solve this, call ModelState.Clear()
removes all the posted back values from ModelState
the helpers will now use the values from the view-model.
Controller:
[HttpPost]
public IActionResult ConfirmDetails(CreateSaleViewModel model)
{
var viewModel = new ConfirmDetailsViewModel
{
ConfirmOrderId = model.OrderId,
...
};
ModelState.Clear(); // force asp-helpers to use the updated model's values
return View("ConfirmDetails", viewModel);
}

ASP.net MVC Html.RadioButtonFor Grouping within Html.DisplayFor

I'm having an issue getting a dynamic set of radio buttons (driven by a CMS) working with Model binding.
So far I've gotten the radios to render, but they behave more like checkboxes, which I believe is down to the name atribute on the input being different. I had thought I'd resolved this by using a parent id to group them, but that doesn't work. On save I want to bind the state (checked / not checked) to the IsSelected property on my Option model. If I pass in a name attribute as part of the RadioButtonFor I can get the radio to work as a group but lose the model binding.
I'm using the same setup with a different Display template for checkboxes which works fine, not sure where I'm going wrong here. Anyone get any suggestions?
Models
public class Question
{
public int QuestionId { get; set; } // 1044
public string QuestionText { get; set; } // Whats your favourite colour
public HelpText HelpText { get; set; }
public IEnumerable<Option> Options { get; set; } // colours
}
public class Option
{
public int OptionId { get; set; } //
public int ParentId { get; set; } // Set as QuestionId for radio button grouping :: 1044
public string Label { get; set; }
public string Value { get; set; }
public bool IsSelected { get; set; }
}
public class RadioOption : Option {} // derived classes to drive DisplayFor template matching
public class CheckboxOption : Option {}
Views
Simplified partial view, which is passed the Question model and is looping through all the options and rendering a display template
<fieldset>
#Html.DisplayFor(x => x.Options)
</fieldset>
Radio button Display Template
#model ViewModels.RadioOption
#Html.RadioButtonFor(m => m.ParentId, Model.Label, Model.OptionId)
#Html.LabelFor(m => m.ParentId, Model.Label)
#Html.HiddenFor(m => m.Value)
#Html.HiddenFor(m => m.OptionId)
#Html.HiddenFor(m => m.Label)
#Html.HiddenFor(m => m.ParentId)
Rendered HTML
<fieldset>
<input id="Questions_5__Options_0__ParentId" name="Questions[5].Options[0].ParentId" type="radio" value="Red">
<label for="Questions_5__Options_0__ParentId">Red</label>
<input id="Questions_5__Options_1__ParentId" name="Questions[5].Options[1].ParentId" type="radio" value="Green">
<label for="Questions_5__Options_1__ParentId">Green</label>
<input id="Questions_5__Options_2__ParentId" name="Questions[5].Options[2].ParentId" type="radio" value="Blue">
<label for="Questions_5__Options_2__ParentId">Blue</label>
<input id="Questions_5__Options_3__ParentId" name="Questions[5].Options[3].ParentId" type="radio" value="Yellow">
<label for="Questions_5__Options_3__ParentId">Yellow</label>
</fieldset>

Model binding not working inside a partial view

The problem I currently have is that in my partial view, the checkboxes are not being binded correctly by the MVC framework. The CaseViewModel.IsCaseSelected property will always be false regardless of whether or not the checkbox is selected. However, if I hardcode the html in the parent view instead of rendering a partial, then the CaseViewModel.IsCaseSelected property will be properly set corresponding to the checkboxes.
My code is shown below.
The form in my view looks something like this:
<div class="form-group">
<div>
<label for="ProfileName">Profile Name:</label>
<input type="text" name="ProfileName"><br>
</div>
#Html.Partial("~/Views/Shared/_CasesSelection.cshtml", Model.Cases_Category1)
<div id="category2-cases">
<p>category-2</p>
<label for="select-all">Select all</label>
<input id="select-all" type="checkbox" onclick="select_all_toggle(this)" />
#for (int i = 0; i < Model.Cases_Category2.Count; i++)
{
#Html.Label(Model.Cases_Category2[i].CasesNumber.ToString())
#Html.CheckBoxFor(model => model.Cases_Category2[i].IsCaseSelected)
}
</div>
<div>
<input type="submit" value="Create" class="btn btn-default" />
</div>
My partial view looks like this:
#model List<Models.CaseViewModel>
<div id="some-case">
<p>some-case</p>
<label for="select-all">Select all</label>
<input id="select-all" type="checkbox" onclick="select_all_toggle(this)" />
#for (int i = 0; i < Model.Count; i++)
{
#Html.Label(Model[i].CaseNumber.ToString())
#Html.CheckBoxFor(model => model[i].IsCaseSelected)
}
</div>
The model it is binded to looks like this:
public class TestProfileVM
{
[Required]
[RegularExpression(#"^[a-zA-Z0-9-_]+$")]
public string ProfileName { get; set; }
public List<CaseViewModel> Cases_Category1 { get; set; }
public List<CaseViewModel> Cases_Category2 { get; set; }
}
And finally, CaseViewModel looks like this:
public class CaseVM
{
public string CaseType { get; set; }
public int CaseNumber { get; set; }
public bool IsCaseSelected { get; set; }
}
Additional information:
Additionally, in my parent view, when I replace Model.Cases_Category2[i] with Model.Cases_Category2.ElementAt(i), the binding does not work correctly. What is the reason for this, and could it be related to the original problem? Thanks.
I guess problem is the name of checkbox inside partial view. Hence you pass part of view model to partial page, name of checkbox will generated upon that model which you pass to partial page.
So compare generated name for checkbox inside parent view with the name of checkbox inside partial view and if they are not same, change name of checkbox inside partial page accroding to the name checkbox inside parent page.

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