I'm new to MVC and feel like I'm missing something conceptually. I'm trying to create a page where I can submit multiple records at once, dynamically adding and removing records via ajax
I've been trying to follow examples like Steven Sanderson and haacked but whenever I submit the page the collection contains 0 records.
Looking at Fiddler I can see both rows being submitted yet something goes missing.
I've seen references to editor templates and html helpers to make this prettier but these have mostly gone over my head and most of the examples I've found are in old versions of MVC as well.
Model
public class Projection
{
public int ID { get; set; }
[Display(Name = "Project")]
public int ProjectId{ get; set; }
[Display(Name = "Employee")]
public int EmployeeId { get; set; }
public decimal Hours { get; set; }
[DisplayFormat(DataFormatString = "{0:MM/yyyy}", ApplyFormatInEditMode = true)]
public DateTime Month { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Project Project { get; set; }
public virtual Employee Employee { get; set; }
}
Controller
// GET: Projections/CreateMulti
[HttpGet]
public IActionResult CreateMulti()
{
ViewBag.Index = 0;
ViewData["EmployeeId"] = new SelectList(_context.Employees.Where(e => e.Active).OrderBy(e => e.FullName), "ID", "FullName");
PopulateClientsDropDownList();
PopulateProjectsDropDownList();
return View(new Projection { Month = DateTime.Today.AddMonths(1).AddDays(1 - DateTime.Today.Day) });
}
[HttpGet]
public IActionResult GetNewRow(int index)
{
ViewBag.Index = index;
ViewData["EmployeeId"] = new SelectList(_context.Employees.Where(e => e.Active).OrderBy(e => e.FullName), "ID", "FullName");
PopulateClientsDropDownList();
PopulateProjectsDropDownList();
return PartialView("CreateMultiPartial", new Projection { Month = DateTime.Today.AddMonths(1).AddDays(1 - DateTime.Today.Day) });
}
// POST: Projections/CreateMulti
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateMulti(ICollection<Projection> ProjectionList)
{
foreach (Projection item in ProjectionList)
{
//do something here
}
return View(ProjectionList);
}
View
#model Test.Models.Projection
#{int Index = 1;
string newRowUrl = #Url.Action("GetNewRow", "Projections", new { index = #Index });}
<form asp-action="CreateMulti">
<table class="table" id="myTable">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.Project.Client)
</th>
<th>
#Html.DisplayNameFor(model => model.Project)
</th>
<th>
#Html.DisplayNameFor(model => model.Employee)
</th>
<th>
#Html.DisplayNameFor(model => model.Month)
</th>
<th>
#Html.DisplayNameFor(model => model.Hours)
</th>
<th></th>
</tr>
</thead>
<tbody>
#{Html.RenderPartial("CreateMultiPartial", Model);}
</tbody>
</table>
<div>Add Another Row</div>
<div>
<input type="submit" value="Submit" class="btn btn-default" />
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$("#addNew").click(function () {
event.preventDefault();
$.ajax({
url: $("#addNew").attr("href"),
cache: false,
success: function (html) {
$('#myTable > tbody:last-child').append(html);
indexIterate();
}
});
});
var currentIndex = 1;
function indexIterate() {
var newHref = $("#addNew").attr("href");
var newerHref = newHref.replace(/(?:index=)[0-9]+/i, "index=" + ++currentIndex);
$("#addNew").attr("href", newerHref);
};
});
</script>
}
Fiddler
Projections.Index=0&ClientID=7&Projections%5B0%5D.ProjectId=40&Projections%5B0%5D.EmployeeId=3&Projections%5B0%5D.Month=10%2F2017&Projections%5B0%5D.Hours=1&Projections.Index=1&ClientID=15&Projections%5B1%5D.ProjectId=19&Projections%5B1%5D.EmployeeId=32&Projections%5B1%5D.Month=10%2F2017&Projections%5B1%5D.Hours=1&__RequestVerificationToken=AAA
Related
I have one table which contains static data with 5 columns and 4 rows. I have another table which would accept input based on static table. I am trying to build a view with table where all field from static Db table is displayed and few text box so that user could fill in the data. But when I click on submit button no data is saved in second table. Can anyone help me with this. How could I submit multiple rows to database based on static database table.
Following are the codes:
View Model:
public class DBM2BillingViewModel
{
public List<DatabaseM2> databaseM2List { get; set; }
[Display(Name = "Items")]
public string Name { get; set; }
[Display(Name = "PO Item No.")]
public int POItemNo { get; set; }
[Display(Name = "Date of Effect")]
public DateTime DateOfEffect { get; set; }
[Display(Name = "Baseline Volume")]
public float baselineVolume { get; set; }
[Display(Name = "Billing Month")]
public string BillingMonth { get; set; }
[Display(Name = "Monthly Device Count")]
public float DeviceCount { get; set; }
}
**Controller**
//Get Method
public IActionResult Create()
{
DBM2BillingViewModel dbm2billingVM = new DBM2BillingViewModel();
dbm2billingVM.databaseM2List = _context.databaseM2s.ToList();
return View(dbm2billingVM);
}
//Post Method
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(DBM2BillingViewModel model)
{
foreach(var dbm2 in model.databaseM2List)
{
DeviceBilling addModel = new DeviceBilling();
addModel.Name = dbm2.Name;
addModel.TrackName = "Database M2";
addModel.POItemNo = dbm2.POItemNo;
addModel.DateOfEffect = dbm2.DateOfEffect;
addModel.baselineVolume = dbm2.BaselineVolume;
addModel.BillingMonth = model.BillingMonth;
addModel.DeviceCount = model.DeviceCount;
_context.deviceBillings.Add(addModel);
}
_context.SaveChanges();
return RedirectToAction(nameof(Index));
}
enter code here
View
#model FinancantPro.Models.DeviceCountModel.DBM2BillingViewModel
#{
ViewData["Title"] = "Create";
}
<div class="container">
<table class="table table-striped border">
<thead>
<tr class="table-info">
<th>#Html.DisplayName("Item Name")</th>
<th>#Html.DisplayName("PO Item No#")</th>
<th>#Html.DisplayName("Date Of Effect")</th>
<th>#Html.DisplayName("Baseline Volume")</th>
<th>#Html.DisplayName("Billing Month")</th>
<th>#Html.DisplayName("Device Count")</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.databaseM2List.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(d => d.databaseM2List[i].Id)
#Html.DisplayFor(d => d.databaseM2List[i].Name)
</td>
<td>
#Html.DisplayFor(d => d.databaseM2List[i].POItemNo)
</td>
<td>
#Html.DisplayFor(d => d.databaseM2List[i].DateOfEffect)
</td>
<td>
#Html.DisplayFor(d => d.databaseM2List[i].BaselineVolume)
</td>
<td>
#Html.TextBoxFor(d => d.BillingMonth, new { #class = "form-control" })
</td>
<td>
#Html.TextBoxFor(d => d.DeviceCount, new { #class = "form-control" })
</td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Create" />
</div>
</div>
**************************
If I add list in View I am not able to resolve the Model
I'm having troubles with my form validation. For some reason the form ignores any [Required] attribute I have in my model.
Model: (Stripped down for easy read)
public class NotEmployeesModel
{
public NotEmployeesModel()
{
DetailModel = new NotEmployeesDetailModel();
}
public NotEmployeesDetailModel DetailModel { get; set; }
}
public class NotEmployeesDetailModel
{
public NotEmployeesDetailModel()
{
}
public NotEmployeesDocumentModel DocumentModel { get; set; }
}
public class NotEmployeesDocumentModel
{
public NotEmployeesDocumentModel()
{
}
public NotEmployeeDocumentInputModel DocumentInput { get; set; }
public class NotEmployeeDocumentInputModel
{
public NotEmployeeDocumentInputModel()
{
}
public NotEmployeeDocumentInputModel(int notEmployeeId)
{
NotEmployeeId = notEmployeeId;
}
public int NotEmployeeId { get; set; }
public int SelectedDocumentType { get; set; }
[Required(ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "Star")]
public string Description { get; set; }
[Required(ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "Star")]
public HttpPostedFileBase File { get; set; }
}
}
For each view or partial I have a separate model class.
Form:
#model NotEmployeesDocumentModel
#using (Html.BeginForm("AddNotEmployeeDocument", "Home", FormMethod.Post, new { id = "form-add-document", enctype = "multipart/form-data" }))
{
#Html.HiddenFor(x => x.DocumentInput.NotEmployeeId)
<table class="table-output">
<thead>
<tr>
<td>#Html.Label("Sort", Labels.Sort)</td>
<td>#Html.Label("Description", Labels.Description)</td>
<td>#Html.Label("Type", Labels.Type)</td>
<td class="text-align-right"></td>
</tr>
</thead>
<tbody>
<tr>
<td>
<i class="fa fa-plus-square cursor-pointer add-document"></i>
<input type="file" id="DocumentInput_File" name="DocumentInput.File" required="required" />
#Html.ValidationMessageFor(x => x.DocumentInput.File)
</td>
<td>
#Html.TextBoxFor(x => x.DocumentInput.Description, new { #class = "width300px hardware-setup-input", placeholder = "Vul hier een omschrijving in..." })
#Html.ValidationMessageFor(x => x.DocumentInput.Description)
</td>
<td>
#Html.DropDownListFor(x => x.DocumentInput.SelectedDocumentType, Model.DocumentTypes, "--- Maak een keuze ---")
</td>
<td class="text-align-right">
<span id="btn-add-document" class="button-org">#Labels.Save</span>
</td>
</tr>
</tbody>
</table>
}
Structure of my page: View / Partial / Partial (Here is my form)
JS:
$("#btn-add-document").on("click", function () {
var frm = $("#form-add-document");
if (frm.valid()) {
frm.ajaxSubmit({
dataType: "html",
success: function (responseText) {
$("#document-container").html(responseText);
$("#DocumentInput_Description").text("");
$("#DocumentInput_SelectedDocumentType").val("");
loadDocumentPartial();
}
});
}
});
I'm using jQuery Form plugin from malsup to submit my form through ajax.
Controller:
[HttpPost]
public ActionResult AddNotEmployeeDocument(NotEmployeesDocumentModel input)
{
// do some code
}
As you can see in the parameters of my ActionResult I can't put NotEmployeesDocumentInputModel like I always do but I'm forced to use the parent class.
I have no idea what I'm doing wrong. This is the first time I encounter such problem.
I am trying to display a form for merging two contacts in ASP.net MVC 5.
The form should look like this where each row holds a radio button group consisting of 2 options:
The form shows up just fine, and the radio groups work (I can select each group). However, when the model is posted back to the server, the Values list is empty (not preserved). I would like to get both the selected id but of course also the actual text value back in the controller. I prefer to do this using Editor Templates and if possible without for loops.
The current results (Values = null):
EDIT to respond to comments: I would prefer not to re-fetch the values in the controller again, because it results in a call to a web service. I have tried some variants of HiddenFor without results.
My models look like this:
public class Contact
{
public List<ContactRow> Rows { get; set; }
public Contact()
{
this.Rows = new List<ContactRow>();
this.Rows.Add(new ContactRow("First Name", "Homer", "Homie"));
this.Rows.Add(new ContactRow("Last Name", "Simpson", "Simson"));
this.Rows.Add(new ContactRow("Email", "mail1", "mail2"));
this.Rows.Add(new ContactRow("Company Phone", "Phone1", "Phone2"));
this.Rows.Add(new ContactRow("Mobile Phone", "Mobile1", "Mobile2"));
}
}
public class ContactRow
{
public int Selection { get; set; }
public string Label { get; set; }
public List<ValueSet> Values { get; set; }
public ContactRow(string Label, string LeftValue, string RightValue, int Selection = 0)
{
if (LeftValue== null) LeftValue= "";
if (RightValue== null) RightValue= "";
this.Label = Label;
this.Selection = Selection;
this.Values = new List<ValueSet>(2);
this.Values.Add(new ValueSet() { ID = 0, ValueText = LeftValue});
this.Values.Add(new ValueSet() { ID = 1, ValueText = RightValue});
}
public ContactRow() { }
}
public class ValueSet
{
public int ID { set; get; }
public string ValueText { set; get; }
}
The Controller:
public ActionResult Index()
{
Contact model = new Contact();
return View(model);
}
public ActionResult MergeContacts(Contact model)
{
return RedirectToAction("Index");
}
And the views:
Index.cshtml
#model RadioTest.Models.Contact
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm("MergeContacts", "Home", FormMethod.Post, new { encType = "multipart/form-data", id = "contactDetailsForm", name = "contactDetailsForm" }))
{
<div>
<table class="table" width="100%">
<tr>
<th style="text-align:left">Label</th>
#*<th style="text-align:right">R1</th>*#
<th style="text-align:left">
Left Value
</th>
#*<th style="text-align:right">R2</th>*#
<th style="text-align:left">
Right Value
</th>
</tr>
#Html.EditorFor(model => model.Rows)
</table>
<input type="submit" />
</div>
}
Editor Template for ContactRow:
ContactRow.cshtml
#model RadioTest.Models.ContactRow
<tr>
<td style="text-align:left">
#Html.DisplayFor(model => model.Label)
</td>
#foreach (var v in Model.Values)
{
<td style="text-align:left">
#Html.RadioButtonFor(model => model.Selection, v.ID) #v.ValueText
</td>
}
</tr>
#Html.HiddenFor(model => model.Label)
Just change your foreach to for:
#model MVCApp.Controllers.ContactRow
<tr>
<td style="text-align:left">
#Html.DisplayFor(model => model.Label)
</td>
#for (int i = 0; i < Model.Values.Count; i++)
{
<td style="text-align:left">
#Html.RadioButtonFor(model => model.Selection, Model.Values[i].ID) #Model.Values[i].ValueText
#Html.HiddenFor(model => Model.Values[i].ID)
#Html.HiddenFor(model => Model.Values[i].ValueText)
</td>
}
</tr>
#Html.HiddenFor(model => model.Label)
My ViewModel is:
public class ObjectiveVM
{
public string DateSelected { get; set; }
public List<string> DatePeriod { get; set; }
public IList<ObList> obList { get; set; }
public class ObList
{
public int ObjectiveId { get; set; }
public int AnalystId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AnalystName { get; set; }
public bool Include { get; set; }
}
}
This is passed to the view, populated as expected - and displays correctly in the view.
My problem is when it is posted back to the controller. My controller code to accept it back is:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Analyst(ObjectiveVM ovm)
ovm.obList is always showing as null:
My View html is:
#model Objectives.ViewModels.ObjectiveVM
#{
ViewBag.Title = "Analyst";
}
<h2>Copy Objectives for Analyst</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Objective</legend>
#Html.DropDownListFor(model => model.DateSelected, new SelectList(Model.DatePeriod))
<table>
<tr>
<th>
#Html.DisplayNameFor(model => model.obList[0].Include)
</th>
<th>
#Html.DisplayNameFor(model => model.obList[0].AnalystName)
</th>
<th>
#Html.DisplayNameFor(model => model.obList[0].Title)
</th>
<th>
#Html.DisplayNameFor(model => model.obList[0].Description)
</th>
</tr>
#foreach (var obList in Model.obList)
{
<tr>
<td>
#Html.HiddenFor(modelItem => obList.ObjectiveId)
#Html.HiddenFor(modelItem => obList.AnalystId)
#Html.HiddenFor(modelItem => obList.Title)
#Html.HiddenFor(modelItem => obList.Description)
#Html.CheckBoxFor(modelItem => obList.Include)
</td>
<td>
#Html.DisplayFor(modelItem => obList.AnalystName)
</td>
<td>
#Html.DisplayFor(modelItem => obList.Title)
</td>
<td>
#Html.DisplayFor(modelItem => obList.Description)
</td>
</tr>
}
</table>
<p>
<input type="submit" value="Copy Selected Objectives" />
</p>
</fieldset>
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Looking in Developer Tools at the Posted form values, they appear to be ok:
Can anyone see any reason the posted form values, are not mapping back onto my viewmodel in the Controller HTTP post?
Thank you, Mark
You need to use a for...loop here, not a foreach....loop.
#for (int idx = 0;idx < Model.obList.Count;idx++){
#Html.HiddenFor(_ => Model.obList[idx].ObjectiveId)
// ... etc....
}
Without the indexer (idx), the model binder will not know how to bind the values back to the right collection item.
When working with collections in my views, I typically write out my markup without the use of helpers:
#for (int i = 0;i < Model.obList.Count();i++){
<input type="hidden" name="ObList[#i].ObjectiveId" id="ObList[#i].ObjectiveId" value="#ObList[i].ObjectiveId" />
<input type="hidden" name="ObList[#i].AnalystId" id="ObList[#i].AnalystId" value="#ObList[i].AnalystId" />
...
}
This will conform to the wire format the model binder expects, and will slot your values into your ViewModel: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
I have an MVC app that I am trying to put together that requires some select lists and drop down lists.
So, I have the following models....
public class Task
{
public int ID { get; set; }
public string Title { get; set; }
......
public virtual ICollection<Monitor> Monitors { get; set; }
public virtual ICollection<Resource> Resources { get; set; }
}
public class Monitor
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IList<Task> Tasks { get; set; }
}
public class Resource
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
.....
public IList<Task> Tasks { get; set; }
The interesting part is that when I display a list of tasks, among the other properties that display just fine, I need to display a list of 'Monitors' and a list of 'Resources' that are assigned to the task in the Index view shown below.
#model IEnumerable<ResourceManager.Models.Task>
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
.....
<th>
#Html.DisplayNameFor(model => model.Title)
</th>
.....
<th>
#Html.DisplayNameFor(model => model.Monitors)
</th>
<th>
#Html.DisplayNameFor(model => model.Resources)
</th>
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
.....
<td>
#Html.DisplayFor(modelItem => item.Title)
</td>
.....
<td>
#if (item.Monitors == null || item.Monitors.Count == 0)
{<span>No Monitors Assigned</span>}
else
{ string.Join(", ", item.Monitors.Select(m => string.Format("{0} {1}", m.FirstName, m.LastName))); }
</td>
<td>
#Html.DisplayFor(modelItem => item.Resources)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
#Html.ActionLink("Details", "Details", new { id=item.ID }) |
#Html.ActionLink("Delete", "Delete", new { id=item.ID })
</td>
</tr>
}
</table>
And here is the controller....
public ActionResult Index()
{
var tasks = from t in db.Tasks where t.IsActive == true select t;
return View(tasks);
}
I would like for the list of Monitors and the list of Resources to display as a string on the Index, Delete and Details Views i.e. 'Monitor 1, Monitor 2, Monitor 3' and 'Resource 1, Resource 2, Resource 3'.
However on the other views (Create and Edit), I want them to appear as a selectable list.
First Create Select list in your controller,
var monitors = //Your linq query
ViewData["ddlList"] = monitors .Select(x => new SelectListItem {
Text = x.FirstName,
Value = x.Id.ToString()
}).ToList();
And then you can use it in your view as follows,
<%=Html.DropDownList("myList") %>
For the display of Monitors/Resources (and since you want them displayed as a comma-delimited list), you can just use string.Join:
<td>
#string.Join(",", Model.Monitors.Select(m => string.Format("{0} {1}", m.FirstName, m.LastName)))
</td>
To be able to actually use Html.DisplayFor, you'd have to create a custom display template so Razor will actually know how to respond. To do so, in your Views folder, create new folder called "DisplayTemplates", and in that, create a new partial view called "Monitors.cshtml" and "Resources.cshtml", strongly-typed to IEnumerable<Monitor> and IEnumerable<Resource>, respectively. Then inside that file, you'll just add roughly the same code as above:
#model IEnumerable<Monitor>
#string.Join(",", Model.Select(m => string.Format("{0} {1}", m.FirstName, m.LastName)))
Then in your view, you can call:
#Html.DisplayFor(m => m.Monitors, "Monitors")
Unfortunately, in this example, you'd have to feed the template name because the default behavior of DisplayFor for a list, is to render the display template multiple times, once for each member of the list. You could do something like:
# Monitor.cshtml
#model Monitor
#Model.FirstName #Model.LastName,
And then:
#Html.DisplayFor(m => m.Monitors)
But your last item would have a comma at the end.
For editing the lists, all you have to do is pass the select lists to your view. Optimally, you'd do this with a view model, but for simplicity's sake, I'll just use ViewBag here. In your controller action:
ViewBag.MonitorChoices = db.Monitors.Select(m => new SelectListItem
{
Value = m.ID.ToString(),
Text = string.Format("{0} {1}", m.FirstName, m.LastName)
});
Then, in your create/edit view:
#Html.ListBoxFor(m => m.Monitors, ViewBag.MonitorChoices)
Try as follows,
var monitors = //Your linq query
List monitorList = new List();
foreach (var monitor in monitors ){
SelectListItem item = new SelectListItem();
item.Text = monitor.FirstName;
item.Value = monitor.Id.ToString();
monitorList .Add(item);
}
ViewData["ddlList"] = monitorList;