Better Way To Edit A View Model in MVC - asp.net-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/

Related

how i can send multivalue to create action

i have a doctor i want add doctor subspecialty to the doctor from sub specialties table many to many relationship
i need to add subspecialties from multiselect list but my controller only add first selection , i want my create controller take all passed subspecialties and create it
my model
public partial class DoctorSubSpecialty
{
public int Id { get; set; }
public Nullable<int> DoctorId { get; set; }
public Nullable<int> SubSpecialtyId { get; set; }
public virtual DoctorProfile DoctorProfile { get; set; }
public virtual SubSpecialty SubSpecialty { get; set; }
}
}
create get action
public ActionResult Create()
{
ViewBag.DoctorId = new SelectList(db.DoctorProfiles, "Id", "FullName");
ViewBag.SubSpecialtyId = new MultiSelectList(db.SubSpecialties, "id", "Name");
return View();
}
create post action
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "Id,DoctorId,SubSpecialtyId")] DoctorSubSpecialty doctorSubSpecialty)
{
DoctorSubSpecialty doctorSub = db.DoctorSubSpecialties.Where(d => d.DoctorId == doctorSubSpecialty.DoctorId & d.SubSpecialtyId == doctorSubSpecialty.SubSpecialtyId).FirstOrDefault();
if (doctorSub == null) {
db.DoctorSubSpecialties.Add(doctorSubSpecialty);
await db.SaveChangesAsync();
}
my view
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>DoctorSubSpecialty</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.DoctorId, "DoctorId", htmlAttributes: new { #class = "control-label col-md-2", #id = "DoctorID" })
<div class="col-md-10">
#Html.DropDownList("DoctorId", null, htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.DoctorId, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.SubSpecialtyId, "SubSpecialtyId", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("SubSpecialtyId",(MultiSelectList)ViewBag.SubSpecialtyId, htmlAttributes: new { #multiple = "multiple", #class = "form-control" })
#Html.ValidationMessageFor(model => model.SubSpecialtyId, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
Create a ViewModel specific to your usecase that can actually transport more than one Id.
I.e. you will need an int[] to bind the selection to.
A ViewModel also helps you to get rid of all this ViewBag and [Bind] nonsense.
public class CreateDoctorSubSpecialtyViewModel {
// These are the selected values to be posted back
public int DoctorId { get; set; }
public int[] SubSpecialtyIds { get; set; }
// These are the possible values for the dropdowns
public IEnumerable<SelectListItem> DoctorProfiles { get; set; }
public IEnumerable<SelectListItem> SubSpecialties { get; set; }
}
GET action - initialize the ViewModel and pass it to the View:
[HttpGet]
public ActionResult Create() {
var doctorProfiles = db.DoctorProfiles.Select(d =>
new SelectListItem {
Text = d.FullName,
Value = d.Id
}
).ToArray();
var subSpecialties = db.SubSpecialties.Select(s =>
new SelectListItem {
Text = s.Name,
Value = s.id
}
).ToArray();
var viewModel = new CreateDoctorSubSpecialtyViewModel {
DoctorProfiles = doctorProfiles,
SubSpecialties = subSpecialties
};
return View("Create", viewModel);
}
View "Create.cshtml" (styling removed for clarity) - tell MVC which ViewModel we want to use with #model:
#model CreateDoctorSubSpecialtyViewModel
#using (Html.BeginForm("Create", "YourControllerName", FormMethod.Post)) {
#Html.DropDownListFor(m => m.DoctorId, Model.DoctorProfiles)
#Html.DropDownListFor(m => m.SubSpecialtyIds, Model.SubSpecialties, new { multiple = "multiple" })
<input type="submit" />
}
POST action - use Linq Contains to test against multiple submitted SubSpecialtyIds:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(CreateDoctorSubSpecialtyViewModel postData) {
DoctorSubSpecialty[] allSelectedSubSpecialities = db.DoctorSubSpecialties
.Where(d => d.DoctorId == postData.DoctorId
&& postData.SubSpecialtyIds.Contains(d.SubSpecialtyId))
.ToArray();
// ...
}
EDIT #Html.DropDownListFor requires an IEnumerable<SelectListItem> as second parameter.

ASP.Net MVC: User must select one check box validate at client side

I have UI where i am showing 3 checkboxes and each refer to different property of model class. i am using jquery unobtrusive validation just by mvc data annotation. i want when user submit form then user has to select one checkbox otherwise client side error message will display and form will not be submitted.
i can do it by jquery but i want to do it by mvc data annotation.
see my model class
public class Customer
{
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "Mail to me")]
public bool SelfSend { get; set; }
[Display(Name = "3rd party")]
public bool thirdParty { get; set; }
[Display(Name = "Others")]
public bool Others { get; set; }
}
Controller
[ValidateAntiForgeryToken()]
[HttpPost]
public ActionResult Index(Customer customer)
{
if (customer.Others == false || customer.SelfSend == false || customer.thirdParty == false)
ModelState.AddModelError("Error", "Must select one option");
return View();
}
with the below code i can validate any checkboxes is selected or not from server side code and add model error which show error at client side.
but i want to do validation by client side using normal data annotation.
see my razor code
<div class="row">
<div class="col-md-8">
<section id="testform">
#using (Html.BeginForm("Index", "Customers", FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
<h4>Enter customer info.</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(m => m.FirstName, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.FirstName, new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.FirstName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.LastName, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.LastName, new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.LastName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
#Html.CheckBoxFor(m => m.SelfSend)
#Html.LabelFor(m => m.SelfSend)
</div>
</div>
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
#Html.CheckBoxFor(m => m.thirdParty)
#Html.LabelFor(m => m.thirdParty)
</div>
</div>
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
#Html.CheckBoxFor(m => m.Others)
#Html.LabelFor(m => m.Others)
</div>
</div>
<div class="col-md-offset-2 col-md-10">
#Html.ValidationMessage("Error", "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
}
</section>
</div>
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
You can try to write a customer model validation attribute.
add CheckBoxAuthAttribute in your one of three validation property.
There is a method protected virtual ValidationResult IsValid(object value, ValidationContext validationContext) in you can override inValidationAttribute.
public class CheckBoxAuthAttribute : ValidationAttribute
{
public CheckBoxAuthAttribute(params string[] propertyNames)
{
this.PropertyNames = propertyNames;
}
public string[] PropertyNames { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
var values = properties
.Select(p => p.GetValue(validationContext.ObjectInstance, null))
.OfType<bool>();
if (values.Contains(true) || (bool)value == true)
{
return null;
}
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
public class Customer
{
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "Mail to me")]
[CheckBoxAuth("thirdParty", "Others", ErrorMessage = "Must select one option"))]
public bool SelfSend { get; set; }
[Display(Name = "3rd party")]
public bool thirdParty { get; set; }
[Display(Name = "Others")]
public bool Others { get; set; }
}
Since you want one of 3 possible options to be selected, then use radio buttons and bind to a property with a required attribute.
Start by defining a view model
public class CustomerVM
{
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
public int? Mailing { get; set; } // see notes below
}
And in the view
#model CustomerVM
....
#using (Html.BeginForm())
{
....
<label>
#Html.RadioButtonFor(m => m.Mailing, 1, new { id = ""})
<span>Mail to me</span>
</label>
<label>
#Html.RadioButtonFor(m => m.Mailing, 2, new { id = ""})
<span>3rd party</span>
</label>
.... // ditto for "Others"
#Html.ValidationMessageFor(m => m.Mailing)
....
}
and the POST method will be
[HttpPost]
public ActionResult Index(CustomerVM model)
{
if(!ModelState.IsValid)
{
return View(model);
}
.... // map to instance of data model, save and redirect
}
Note that if these options are unlikely to change, it would be more appropriate to make the property an enum rather than an int, for example
public enum Mailing
{
[Display(Name = "Mail to me")]
SelfSend = 1,
[Display(Name = "3rd party")]
ThirdParty = 2,
[Display(Name = "Others")]
Others = 3
}
public class CustomerVM
{
....
[Required]
public Mailing? Mailing { get; set; }
}
and the view code would be
#Html.RadioButtonFor(m => m.Mailing, Mailing.SelfSend, new { id = ""})

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.

ViewModel Contents are Null after Posting Form to Controller

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

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