ViewModel Contents are Null after Posting Form to Controller - asp.net-mvc

So the ViewModel has 2 sets of data.
The CurrentDetails and UpdatedDetails. Both are instances of the same class which carries strings and whatnot inside etc.
This method has worked with all other views and models I've attempted with, but for THIS one instance, when the form is posted to the controller, its contents (CurrentDetails and UpdatedDetails) are both found to be null.
I've tried changing the parameter name from model to test and to other arbitrary things, but to no avail.
The one thing that worked (but is not a solution to me) is NOT having instances of the class inside the ViewModel, and just having the data there (but I don't see why I should be forced to do things this way.
Here's the controller:
[HttpPost]
public ActionResult FloristProfile(MerchantFloristProfileViewModel test)
{
if (!ModelState.IsValid)
return View(test);
using (var db = new ApplicationDbContext())
{
Florist florist = db.Florists.Find(MerchantBase.FloristID);
if (Request.Form["editSubmit"] != null)
{
florist.Name = test.UpdatedDetails.Name;
florist.Website = test.UpdatedDetails.Website;
db.SaveChanges();
return RedirectToAction("FloristProfile");
}
else if (Request.Form["photoSubmit"] != null)
{
if (test.CurrentDetails.File.ContentLength > 0)
{
CloudBlobContainer container = FlowerStorage.GetCloudBlobContainer();
string blobName = String.Format("florist_{0}.jpg", Guid.NewGuid().ToString());
CloudBlockBlob photoBlob = container.GetBlockBlobReference(blobName);
photoBlob.UploadFromStream(test.CurrentDetails.File.InputStream);
florist.LogoPath = blobName;
florist.isRendering = true;
db.SaveChanges();
return RedirectToAction("FloristProfile");
}
}
}
return Content("Invalid Request");
}
View:
#using (Html.BeginForm("FloristProfile", "Merchant", FormMethod.Post, new { #class = "form-horizontal" }))
{
#Html.ValidationSummary(false, "", new { #class = "text-danger" })
#Html.HiddenFor(x => x.CurrentDetails.FloristID)
#Html.HiddenFor(x => x.CurrentDetails.Name)
#Html.HiddenFor(x => x.CurrentDetails.StaffCount)
#Html.HiddenFor(x => x.CurrentDetails.StoreCount)
#Html.HiddenFor(x => x.CurrentDetails.Website)
<div class="form-group">
#Html.LabelFor(x => x.UpdatedDetails.Name, new { #class = "col-sm-2 control-label" })
<div class="col-sm-10">
#Html.TextBoxFor(x => x.UpdatedDetails.Name, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(x => x.UpdatedDetails.Website, new { #class = "col-sm-2 control-label" })
<div class="col-sm-10">
#Html.TextBoxFor(x => x.UpdatedDetails.Website, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="editSubmit" class="btn btn-success">Save</button>
</div>
</div>
}
ViewModel:
public class MerchantFloristProfileViewModel
{
public class FloristProfileDetails
{
public int FloristID { get; set; }
[Required(ErrorMessage = "Please Enter a Name")]
public string Name { get; set; }
[DataType(DataType.Url)]
[Required(ErrorMessage = "Please Enter a Website")]
public string Website { get; set; }
public int StoreCount { get; set; }
public int StaffCount { get; set; }
// For Picture Upload
public HttpPostedFileBase File { get; set; }
}
public FloristProfileDetails CurrentDetails;
public FloristProfileDetails UpdatedDetails;
}

Both CurrentDetails and UpdatedDetails in your MerchantFloristProfileViewModel model are fields, not properties (no getter/setter) so the DefaultModelBinder cannnot set the values. Change them to
public FloristProfileDetails CurrentDetails { get; set; }
public FloristProfileDetails UpdatedDetails { get; set; }
But you should not be sending all that extra data to the view, then sending it all back again unchanged. Apart from the extra overhead, any malicious user could alter the values in the hidden fields causing your app to fail. Just get the original from the repository again if you need it in the POST method

Related

Stay on same page if form is not valid

public ActionResult Add(Models.ContactModel contact)
{
if (ModelState.IsValid)
{
DAL.Contact mappedContact = Mapper.Map<Models.ContactModel, DAL.Contact>(contact);
repository.AddContact(mappedContact);
return RedirectToAction("Index");
}
else
/* What to return here */
}
This is the controller for adding contact to the database. I am validating the form using data annotations if the form is valid i am redirecting it to the index page. If it is not valid it should stay on the same page showing error message. what to write in else part. can any one suggest me.There is no view for Add Controller.
<div>
<label>Name</label>
#Html.ValidationMessageFor(model => model.Name, null, new { #class = "error-message"})
#Html.TextBoxFor(model => model.Name, new { #class = "long-box" })
</div>
<div>
<label>Email</label>
#Html.ValidationMessageFor(model => model.Email, null, new { #class = "error-message" })
#Html.TextBoxFor(model => model.Email, new { #class = "long-box" })
</div>
<div class="mob-land-container">
<label>Mobile</label>
#Html.ValidationMessageFor(model => model.MobileNumber, null, new { #class = "error-message" }) <br>
#Html.TextBoxFor(model => model.MobileNumber, new { #class = "short-box" })
</div>
<div class="mob-land-container" id="landline-container">
<label>Landline</label>
#Html.ValidationMessageFor(model => model.LandlineNumber, null, new { #class = "error-message" })<br>
#Html.TextBoxFor(model => model.LandlineNumber, new { #class = "short-box" })
</div>
<div>
<label>Website</label>
#Html.ValidationMessageFor(model => model.Website, null, new { #class = "error-message" })
#Html.TextBoxFor(model => model.Website, new { #class = "long-box" })
</div>
<div>
<label>Address</label>
#Html.ValidationMessageFor(model => model.Address, null, new { #class = "error-message" })
#Html.TextAreaFor(model => model.Address, new { #class = "address-box" })
</div>
</div>
<div class="button-container">
<input type="button" id="cancel" value="Cancel" onclick="location.href='#Url.Action("Index", "Contact")'" />
<input type="submit" id="add" value="Add" onclick="location.href='#Url.Action("Add", "Contact")'" />
</div>
This is the form where i am getting data to controller.
public class ContactModel
{
public int Id { get; set; }
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[Required(ErrorMessage = "Email is required")]
public string Email { get; set; }
[Required(ErrorMessage = "Mobile Number is required")]
public string MobileNumber { get; set; }
[Required(ErrorMessage = "Landline Number is required")]
public string LandlineNumber { get; set; }
[Required(ErrorMessage = "Website is required")]
public string Website { get; set; }
[Required(ErrorMessage = "Address is required")]
public string Address { get; set; }
}
This is the model class.
Thanks in advance.
I like to flip the login in situations like this. If the model isn't valid just return it back to the view. the model binder on the POST will take care of the validations and once you send the model back to the view, you will see the individual validations on the screen.
If you have any dropdown, you will need to re-populate them before sending sending the model back.
public ContactController : Controller
{
[HttpGet]
public ActionResult Add()
{
return View(new Models.ContactModel());
}
[HttpPost]
public ActionResult Add(Models.ContactModel contact)
{
if (!ModelState.IsValid)
{
return View(contact);
}
DAL.Contact mappedContact = Mapper.Map<Models.ContactModel, DAL.Contact>(contact);
repository.AddContact(mappedContact);
return RedirectToAction("Index");
}
}
The GET action returns the empty form.
The POST action posts the model to the server.
Your view model should be named Add.cshtml to that mvc can automatically pick it up.
And change your view buttons
<div class="button-container">
#Html.ActionLink("Cancel", "Index", "Contact")
<input type="submit" value="Save" />
</div>
Style the Cancel link to look like a button
Your submit will automatically submit to the Add POST method.
The model state check returns the model back to the view with the validation information in it so that you can correct the form.

Repopulating selectlist for dropdownlist on postback when default selected ASP.NET MVC

New to ASP.NET MVC and I've come across this before, however I feel I've gone about fixing it in an incorrect way. I was hoping someone could point me in the direction of the correct way of doing it.
I have a page which the user selects a dropdown value and clicks next. Now, if they don't select an item and select the default value ("Select..."), there is a validation error. The controller seems to lose information about the dropdownlist even though the model is returned to the view on a "postback." So, say I don't basically repeat code in my [HTTPPost] from my [HTTPGet]. I get the error:
There is no ViewData item of type 'IEnumerable<SelectListItem>' that has the key 'SheetIndex'.
on
#Html.DropDownListFor(model => model.SheetIndex, Model.SheetsDropdown, "Select...", new { #class = "form-control" })
So unless I repeat code, I get that error when no selection is made. What am I doing wrong?
ViewModel:
public class SelectSheetViewModel
{
public int? Id { get; set; }
public string Name { get; set; }
[Required]
public string SheetIndex { get; set; }
public string SheetName { get; set; }
public int? ChainId { get; set; }
public int? SheetId { get; set; }
public int? FileId { get; set; }
public IEnumerable<SelectListItem> SheetsDropdown { get; set; }
public HeaderViewModel Header { get; set; }
}
Controller:
[HttpGet]
public ActionResult SelectSheet(int? chainId, int? sheetId, int? fileId)
{
if (sheetId == null || fileId == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var fileDetails = db.FileDetails.Find(fileId);
SelectSheetViewModel selectSheet = new SelectSheetViewModel()
{
Id = fileDetails.FileId,
Name = fileDetails.Name,
ChainId = chainId,
SheetId = sheetId,
FileId = fileId
};
string fileName = fileDetails.UniqueName + fileDetails.Extension;
string relativeFileLocation = "~/uploads/" + fileName;
string absoluteFileLocation = HttpContext.Server.MapPath(relativeFileLocation);
if (System.IO.File.Exists(absoluteFileLocation))
{
DSDBuilder builder = new DSDBuilder();
selectSheet.SheetsDropdown = builder.GetSheets(absoluteFileLocation); // Where I get my selectlist
}
else
{
ModelState.AddModelError("SheetDropdown", "Excel workbook does not exist.");
}
selectSheet.Header = BuildHeaderViewModel(chainId, sheetId);
return View(selectSheet);
}
[HttpPost]
public ActionResult SelectSheet(SelectSheetViewModel selectSheet)
{
if (ModelState.IsValid)
{
FileDetail fileDetails = db.FileDetails.Find(selectSheet.FileId);
string[] sheetIndexAndName = selectSheet.SheetIndex.Split(':');
fileDetails.SheetIndex = Convert.ToInt32(sheetIndexAndName[0]);
fileDetails.SheetName = sheetIndexAndName[1];
db.SaveChanges();
return RedirectToAction("Build", "Sheets", new
{
ChainId = selectSheet.ChainId,
SheetId = selectSheet.SheetId,
FileId = selectSheet.FileId
});
}
// Probably not a good method vvv
var fileDetailsPostBack = db.FileDetails.Find(selectSheet.FileId);
string fileName = fileDetailsPostBack.UniqueName + fileDetailsPostBack.Extension;
string relativeFileLocation = "~/uploads/" + fileName;
string absoluteFileLocation = HttpContext.Server.MapPath(relativeFileLocation);
if (System.IO.File.Exists(absoluteFileLocation))
{
DSDBuilder builder = new DSDBuilder();
selectSheet.SheetsDropdown = builder.GetSheets(absoluteFileLocation);
}
else
{
ModelState.AddModelError("SheetDropdown", "Excel workbook does not exist.");
}
// Probably not a good method ^^^
return View(selectSheet);
}
View:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ChainId)
#Html.HiddenFor(model => model.SheetId)
#Html.HiddenFor(model => model.FileId)
#Html.HiddenFor(model => model.Header.ChainName)
#Html.HiddenFor(model => model.Header.SheetName)
#Html.HiddenFor(model => model.Header.SheetDescription)
<div class="form-horizontal">
<div class="form-group">
#Html.LabelFor(model => model.SheetIndex, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.SheetIndex, Model.SheetsDropdown, "Select...", new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.SheetIndex, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Next" class="btn btn-default" />
</div>
</div>
</div>
}
I also would not like to repeat that code because it goes to a function which uses Excel.Interop and is kind of resource heavy. Let me know if anyone has a better solution. I'm always trying to improve my code and do things the "correct" way.

Better Way To Edit A View Model in MVC

I actually came up with a working example of how to display (GET) and edit (POST) a view model consisting of three models in MVC. However, my MVC skills are limited and I was looking for suggestions on "the right way" I should be doing this.
The part in question is that I'm sending the form fields back individually rather than to a view model, which is something I couldn't figure out how to do.
Here are my models
Name
public partial class Name
{
public Name()
{
this.Addresses = new HashSet<Address>();
this.Emails = new HashSet<Email>();
}
public int ID { get; set; }
public string FIRST_NAME { get; set; }
public string LAST_NAME { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
public virtual ICollection<Email> Emails { get; set; }
}
Address
public partial class Address
{
public int ADDRESS_ID { get; set; }
public int NameID { get; set; }
public string ADDRESS_1 { get; set; }
public string CITY { get; set; }
public string STATE { get; set; }
public string ZIP { get; set; }
public virtual Name Name { get; set; }
}
Email
public partial class Email
{
public int EMAIL_ID { get; set; }
public int NameID { get; set; }
public string EMAIL { get; set; }
public virtual Name Name { get; set; }
}
My View Model made up of all the fields from the three models.
public class ContactFormViewModel
{
public int? ID { get; set; }
public string FIRST_NAME { get; set; }
public string LAST_NAME { get; set; }
public string ADDRESS_1 { get; set; }
public string CITY { get; set; }
public string STATE { get; set; }
public string ZIP { get; set; }
public string EMAIL { get; set; }
}
The GET method of the Edit page (in the Controller)
// GET: Names/Edit/5
//The GET method takes the id from the URL and passes it into the query to return data for the specific record
public ActionResult Edit(int id)
{
//This query is an outer join of the Name, Address and Email models/tables
var query = from n in db.Names
join a in db.Addresses
on n.ID equals a.NameID into na
from a in na.DefaultIfEmpty()
join e in db.Emails
on n.ID equals e.NameID into ne
from e in ne.DefaultIfEmpty()
where n.ID == id
//Creates a new instance of the view model, populated with the query data
select new ContactFormViewModel
{
ID = id,
FIRST_NAME = n.FIRST_NAME,
LAST_NAME = n.LAST_NAME,
ADDRESS_1 = a.ADDRESS_1,
CITY = a.CITY,
STATE = a.STATE,
ZIP = a.ZIP,
EMAIL = e.EMAIL
};
//Returns the query to the view
return View(query);
}
The POST method of the Edit page (in the Controller)
// POST: Names/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
//The POST method takes the individual form field data and passes it to queries that update all three models separately
public ActionResult Edit(int id, string FIRST_NAME, string LAST_NAME, string ADDRESS_1, string CITY, string STATE, string ZIP, string EMAIL)
{
if (ModelState.IsValid)
{
//Query the database for the row to be updated.
var queryN =
from n in db.Names
where n.ID == id
select n;
var queryA =
from a in db.Addresses
where a.NameID == id
select a;
var queryE =
from e in db.Emails
where e.NameID == id
select e;
//Assign the form field data to the fields in the model
foreach (Name n in queryN)
{
n.FIRST_NAME = FIRST_NAME;
n.LAST_NAME = LAST_NAME;
}
//If there are no address records, insert
if (!queryA.Any())
{
//New instance of Address
var address = new Address
{
NameID = id,
ADDRESS_1 = ADDRESS_1,
CITY = CITY,
STATE = STATE,
ZIP = ZIP
};
db.Addresses.Add(address);
}
//Else, if there are address records, then update
else
{
foreach (Address a in queryA)
{
a.ADDRESS_1 = ADDRESS_1;
a.CITY = CITY;
a.STATE = STATE;
a.ZIP = ZIP;
}
}
//If there are no email records, insert
if (!queryE.Any())
{
//New instance of Email
var email = new Email
{
NameID = id,
EMAIL = EMAIL
};
db.Emails.Add(email);
}
//Else, if there are email records, then update
else
{
foreach (Email e in queryE)
{
e.EMAIL = EMAIL;
}
}
//// Submit the changes to the database.
try
{
db.SaveChanges();
}
catch (Exception ex)
{
Console.WriteLine(ex);
// Provide for exceptions.
}
}
return RedirectToAction("Index");
}
The View
#model IQueryable<MDTestApplication.ViewModel.ContactFormViewModel>
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Name</h4>
<hr />
#*Uses foreach loop to get all field data from the view model*#
#foreach (var item in Model)
{
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(modelItem => item.ID)
<div class="form-group">
#Html.LabelFor(modelItem => item.FIRST_NAME, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#*Using razor syntax to output the value*#
#*Using form field 'Name' attribute for posting back to the controller*#
<input type="text" name="FIRST_NAME" value="#item.FIRST_NAME" class="form-control" />
</div>
</div>
<div class="form-group">
#Html.LabelFor(modelItem => item.LAST_NAME, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="text" name="LAST_NAME" value="#item.LAST_NAME" class="form-control" />
</div>
</div>
<div class="form-group">
#Html.LabelFor(modelItem => item.ADDRESS_1, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="text" name="ADDRESS_1" value="#item.ADDRESS_1" class="form-control" />
</div>
</div>
<div class="form-group">
#Html.LabelFor(modelItem => item.CITY, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="text" name="CITY" value="#item.CITY" class="form-control" />
</div>
</div>
<div class="form-group">
#Html.LabelFor(modelItem => item.STATE, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="text" name="STATE" value="#item.STATE" class="form-control" />
</div>
</div>
<div class="form-group">
#Html.LabelFor(modelItem => item.ZIP, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="text" name="ZIP" value="#item.ZIP" class="form-control" />
</div>
</div>
<div class="form-group">
#Html.LabelFor(modelItem => item.EMAIL, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="text" name="EMAIL" value="#item.EMAIL" class="form-control" />
</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>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
UPDATE
Here's additional insert and update code I added to Alex's answer below. You would use this same setup for address and email.
foreach (var address in model.Addresses)
{
var addressToUpdate = name.Addresses.FirstOrDefault(a => AddressId== address.AddressId);
if (addressToUpdate != default(Address))
{
// preform an update
addressToUpdate.AddressId = address.AddressId;
addressToUpdate.City = address.City;
addressToUpdate.State = address.State;
addressToUpdate.Zip = address.Zip;
}
else
{
//perform an insert
var newAddress = new Address
{
NameID = model.ID,
Address1 = address.Address1,
City = address.City,
State = address.State,
Zip = address.Zip
};
db.Addresses.Add(newAddress);
}
}
First of all let me start with the naming convention.
This:
public int ADDRESS_ID { get; set; }
public int NameID { get; set; }
Is BAD You have no naming convention at all, some properties are PascalCase, others capital case with underscores. I strongly advise you to install some tool that will enforce you to apply set of style and consistency rules(for example StyleCop). In general it is very common to use PascalCase for properties.
Once you apply it your models will look like:
public partial class Address
{
public int AddressId { get; set; }
public int NameId { get; set; }
public string Address1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public virtual Name Name { get; set; }
}
Second thing:
If I understand correct you are trying to edit a data for one user:
His(or hers) first and last name, list of addresses and a lit of emails. If I am right both your View and ViewModel are wrong. Your ViewModel could look as following:
public class ContactFormViewModel
{
public int NameId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IList<Address> Addresses { get; set; }
public IList<Emails> { get; set; }
}
Controller(UPDATED):
// GET: Names/Edit/5
//The GET method takes the id from the URL and passes it into the query to
//return data for the specific record
public ActionResult Edit(int id)
{
//You don't need the joins since you have navigation properies!
var name = db.Names.FirstOrDefault(n => n.ID == id);
ContactFormViewModel model;
if(name == default(Name))
{
model = new ContactFormViewModel{
Addresses = new List<Address>(),
Emails = new List<Email>()
};
}
else {
model = new ContactFormViewModel
{
NameId = name.NameId ,
FirstName = name.FirstName,
LastName = name.LastName ,
Addresses = name.Addresses.ToList(),
Emails = name.Emails.ToList(),
};
}
if(!model.Addresses.Any())
{
model.Addresses.Add(new Address());
}
if(!model.Emails.Any())
{
model.Emails.Add(new Email());
}
//Returns the query to the view
return View(model);
}
View:
#model MDTestApplication.ViewModel.ContactFormViewModel
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Name</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.NameId)
<div class="form-group">
#Html.LabelFor(model => model.FirstName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FirstName, new { #class = "FirstName" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.LastName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => item.LastName, new { #class = "FirstName" })
</div>
</div>
#for (int i = 0; i < Model.Addresses.Count; i++)
{
#Html.HiddenFor(model => model.Addresses[i].AddressId)
<div class="form-group">
#Html.LabelFor(model => model.Addresses[i].Address1, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Addresses[i].Address1, new { #class = "FirstName" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Addresses[i].City, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Addresses[i].City, new { #class = "FirstName" })
</div>
</div>
/// Continue here with all the address properties
}
#for (int i = 0; i < Model.Emails.Count; i++)
{
#Html.HiddenFor(model => model.Emails[i].EmailId)
<div class="form-group">
#Html.LabelFor(model => model.Emails[i].Email, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Emails[i].Email, new { #class = "FirstName" })
</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>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
Edit Action in Controller:
public ActionResult Edit(ContactFormViewModel model)
{
if (ModelState.IsValid)
{
//Query the database for the row to be updated.
var name = db.Names.FirstOrDefault( n => n.NameId == model.NameId);
if(name != default(Name))
{
name.FirstName = model.FirstName;
name.LastName = model.LastName;
bool hasAddresses = name.Addresses.Any();
foreach(var address in model.Addresses)
{
var addressToUpdate = name.Addresses.FirstOrDefault(a => a.AddressId == address.AddressId);
if(addressToUpdate != default(Address))
{
// preform an update
}
else
{
//perform an insert
}
}
foreach(var email in model.Emails)
{
var emailToUpdate = name.Emails.FirstOrDefault(a => a.EmailId == email.EmailId);
if(emailToUpdate != default(Email))
{
// preform an update
}
else
{
//perform an insert
}
}
db.SaveChanges();
}
}
}
What you are attempting to do is submit a variable length list, which is not something asp mvc can handle on its own. Let me explain:
You have a form, and inside the form is a foreach loop that creates editable fields for multiple records. ASP MVC can handle updating a single record just fine with its form helpers, however when you have multiple records that need updating, ASP MVC doesn't automatically create an indexing number for each record, which makes it impossible for it to keep track of which property belongs to which record. When you submit the form, MVC doesn't create this indexing for you, so you need to find alternative solutions.
I highly suggest you look at this blog and incorporate the helper that is provided to achieve your form submission:
http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/

On posting to server all collections of complex types are null in mvc

I'm fairly new to MVC but am progressing.
I have come across an issue that I can't seem to solve and would be greatful of any assistance.
When I post to the server my edits (in RoutineViewModel) are mostly lost, primitive data types are persisted (in class Routine) but collections of complex types (ICollection<RoutineExercise>) are lost.
I found this MVC Form not able to post List of objects and followed the advice to seperate the view into an EditorTemplate but this has not worked. Using the '#foreach' loop still produces all the page controls with the same id and name when you viewsource. I tried using a for (int i = 1; i <= 5; i++) type loop as many other posts suggest but get errors about not being able to apply index to my object.
Also the fact this #Html.DropDownListFor(model => Model.ExerciseId, Model.Exercises, "", new { #class = "input-sm col-md-12" }) does not select the correct list item (Model.ExerciseId has the correct value) concerns me.
Any help/advice would be great as I'm stuck and have been for 3 days now.
* POCO *
public partial class Routine
{
public Routine()
{
this.RoutineExercises = new List<RoutineExercise>();
}
public int Id { get; set; }
public string RoutineName { get; set; }
public string Description { get; set; }
...Other fields removed for clarity...
public virtual ICollection<RoutineExercise> RoutineExercises { get; set; }
}
public partial class RoutineExercise
{
public int Id { get; set; }
public int RoutineId { get; set; }
public int Exerciseid { get; set; }
public int SetsToDo { get; set; }
public int RepsToDo { get; set; }
...Other fields removed for clarity...
public virtual Exercise Exercise { get; set; }
public virtual Routine Routine { get; set; }
}
* VIEWMODEL *
public class RoutineViewModel
{
//Routine information
public int Id { get; set; }
[Display(Name = "Name")]
public string RoutineName { get; set; }
public string Description { get; set; }
//Exercise information
[Display(Name = "Exercise")]
public ICollection<RoutineExercise> RoutineExercises { get; set; }
public IEnumerable<SelectListItem> Exercises { get; set; }
public int ExerciseId { get; set; }
}
* FORM *
<div class="panel-body">
#using (Html.BeginForm("Edit", "Workout"))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Id)
#Html.EditorForModel()
<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>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
</div>
* EDITOR TEMPLATE *
<div class="form-group">
#Html.LabelFor(model => model.RoutineName, new { #class = "control-label col-md-1" })
<div class="col-md-2">
#Html.EditorFor(model => model.RoutineName)
#Html.ValidationMessageFor(model => model.RoutineName)
</div>
#Html.LabelFor(model => model.Description, new { #class = "control-label col-md-1" })
<div class="col-md-2">
#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)
</div>
</div>
#foreach (var e in Model.RoutineExercises)
{
#Html.LabelFor(model => model.RoutineExercises, new { #class = "control-label col-md-1" })
<div class="col-md-3">
#*TO FIX This does NOT bind the selected value*#
#Html.DropDownListFor(model => Model.ExerciseId, Model.Exercises, "", new { #class = "input-sm col-md-12" })
</div>
<div class="col-md-12">
#Html.LabelFor(model => e.SetsToDo, new { #class = "control-label col-md-2" })
#Html.EditorFor(m => e.SetsToDo, new { #class = "control-label col-md-10" })
</div>
}
* CONTROLLER *
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(RoutineViewModel rvm) /*rvm always null for collections only*/
{
if (ModelState.IsValid)
{
//Save Routine
var r = new Routine
{
Id = rvm.Id,
RoutineName = rvm.RoutineName,
Description = rvm.Description,
RoutineFrequencyId = rvm.RoutineFrequencyId,
RoutineLengthId = rvm.RoutineLengthId
};
_repo.Update(r);
return RedirectToAction("Index");
}
return View(getRoutineViewModel(rvm.Id));
}
First, avoid the term "complex type" unless you're actually talking about a complex type in Entity Framework. It just creates confusion, and honestly, nothing you have here is really "complex" anyways.
You will indeed need to employ a for loop with an index instead of foreach to get the proper field names for the modelbinder to work with. However, the reason you're getting an error is that ICollection is not subscriptable ([N]). You can use ElementAt(N) to pull out the item at an index, but unfortunately, Razor will still not create the right field names with that. As a result, you need to use something like List for your collection properties to edit them inline. Since you're already using a view model this is trivial. Just change the property type from ICollection<RoutineExcercise> to List<RoutineExcercise> on your view model.

How can you pass a list of objects in a model back to a controller? [duplicate]

This question already has answers here:
Model Binding to a List MVC 4
(3 answers)
Closed 9 years ago.
UPDATE: The solution was to use an EditorTemplate. See solution below:
I want to pass a model to/from a controller which let's me set name, and set the value on an undetermined roles (as checkboxes). When I examine the postback, I get a value for Name in model, but Roles is null. How can I tell which checkboxes were checked?
Model:
public class MyModel
{
public string Name { get; set; }
public IEnumerable<RoleItem> Roles { get; set; }
}
public class RoleItem
{
public String Name { get; set; }
public String Id { get; set; }
public bool Selected { get; set; }
public RoleItem(String id, String name, bool selected = false)
{
this.Name = name;
this.Id = id;
this.Selected = selected;
}
}
Razor:
#model WebApplication1.Controllers.MyModel
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
#Html.TextBoxFor(m=>m.Name)
foreach (var m in Model.Roles)
{
<div>
#Html.Label(m.Id, m.Name)
#Html.CheckBox(m.Id, m.Selected, new { id = #m.Id })
</div>
}
<input type="submit"/>
}
GOAL: To allow any Administrator to add new users to the Asp identity tables and assign them roles that are defined in a list using checkboxes.
Model:
public class RegisterViewModel
{
[Display(Name = "Name")]
public string FullName { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public List<RoleItem> Roles { get; set; }
}
public class RoleItem
{
public String Name { get; set; }
public String Id { get; set; }
public bool IsMember { get; set; }
}
Controller (GET): This reads all of the roles in the database and transforms them to a list of RoleItems. This will prepend the character "r" onto the id field as some browsers have a problem with an id starting with a number. We want to make sure the "Users" group is checked by default, so we find this value in the list and set the IsMember property to true. We check the request to see if the page was redirected here from a successful POST (see below)
// GET: /Account/AddUser
[Authorize(Roles = "Administrators")]
public ActionResult AddUser()
{
var rolesDb = new ApplicationDbContext(); //Users, Administrators, Developers, etc
ViewBag.AddSuccess = Request["added"]=="1" ? true : false;
var roleItems = rolesDb.Roles.Select(r => new RoleItem() { Id = "r" + r.Id, Name = r.Name, IsMember = false }).ToList(); //add an r to get around a browser bug
var users = roleItems.FirstOrDefault(r => r.Name == "Users"); //Get the row that has the Users value and set IsMember=true
if (users != null)
users.IsMember = true;
var m = new RegisterViewModel() {Roles = roleItems};
return View(m);
}
View: Pretty standard stuff. Note #Html.EditorFor(x => x.Roles) at the bottom, which uses an editor template (follows)
#model cherry.Models.RegisterViewModel
#{
ViewBag.Title = "AddUser";
}
<h2>#ViewBag.Title.</h2>
#if (Convert.ToBoolean(ViewBag.AddSuccess))
{
<text>User added!</text>
}
#using (Html.BeginForm("AddUser", "Account", FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
<h4>Create a new account.</h4>
<hr />
#Html.ValidationSummary()
<div class="form-group">
#Html.LabelFor(m => m.EmailAddress, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.EmailAddress, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.FullName, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.FullName, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Password, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.PasswordFor(m => m.Password, new { value = Model.Password, #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.ConfirmPassword, new {#class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.PasswordFor(m => m.ConfirmPassword, new {value = Model.ConfirmPassword, #class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
#Html.EditorFor(x => x.Roles)
}
EditorTemplate:
You MUST give the template the same name as the object you are creating the template for. You must also put this object in a folder called EditorTemplates below the view you are designing this for, or you can put the folder inside the shared folder.
Views\Account\EditorTemplates\RoleItem.cshtml
#model cherry.Models.RoleItem
<div>
#Html.CheckBoxFor(x => x.IsMember)
#Html.LabelFor(x => x.IsMember, Model.Name)
#Html.HiddenFor(x => x.Name)
</div>

Resources