Visual Studio 2012
MVC 5.2.0
jQuery 2.1.1
jQuery UI Combined 1.10.4
Json.Net 6.0.3
Kendo UI MVC 2014.1.528
I've read through many similar posts. I've tried to apply what I've seen to no avail. I throw myself at feet of your keyboards for your mercy and help.
I have a survey-like site, so for simplicity here, I made up a sample project to duplicate the issue. I can upload that too.
I have a model with a child object of Address - no problemo there - it binds.
The model also has a collection of questions. It never binds on the post, and this is the issue here.
Lets start with models:
The Survey itself:
public class Survey
{
[ScaffoldColumn(false)]
public int Id { get; set; }
public string Name { get; set; }
[DisplayName("Phone #")]
[StringLength(15)]
[DataType(DataType.PhoneNumber)]
public string ContactMethodPhone { get; set; }
[DisplayName("Email")]
[StringLength(120)]
[EmailAddress]
public string ContactMethodEmail { get; set; }
public virtual Address Address { get; set; }
public virtual List<Question> Questions { get; set; }
}
and the address:
public class Address
{
[ScaffoldColumn(false)]
public int AddressId { get; set; }
[DisplayName("Address line 1")]
[Required]
[StringLength(200)]
public string Street { get; set; }
[DisplayName("Address line 2")]
[StringLength(200)]
public string Street2 { get; set; }
[DisplayName(" ")]
[StringLength(50)]
public string ApartmentNum { get; set; }
[DisplayName("Type")]
[StringLength(50)]
public string Tenement { get; set; }
[DisplayName("City")]
[Required]
[StringLength(200)]
public string City { get; set; }
[DisplayName("Province/State")]
[StringLength(20)]
public string State { get; set; }
[Required]
[DisplayName("Postal/Zip Code")]
[StringLength(10)]
public string MailCode { get; set; }
[DisplayName("Country")]
[StringLength(30)]
public string Country { get; set; }
[NotMapped]
public List<SelectListItem> Cities { get; set; }
[NotMapped]
public List<SelectListItem> Provinces { get; set; }
[NotMapped]
public List<SelectListItem> Countries { get; set; }
[NotMapped]
public List<SelectListItem> MailCodes { get; set; }
[NotMapped]
public List<SelectListItem> Tenements { get; set; }
}
and the questions:
public class Question
{
[ScaffoldColumn(false)]
public int Id { get; set; }
[ScaffoldColumn(false)]
public int ImageID { get; set; } // Question Image
[DisplayName("Question: ")]
public string InformationIntakeGroupValue { get; set; } // Question: ie Did you graguate high school
public int ImageID_Topic { get; set; } // Topic Image
[DisplayName("Topic: ")]
public string InformationIntakeTopicValue { get; set; } // Topic: ie Education
[ScaffoldColumn(false)]
public string InformationIntakeTypeCode { get; set; } // Type of question (date, bool, text)
// below not filled by select;
// the key from the EntityAffilliateIntake record insert
public int PersonId { get; set; } // Person anwering question
// this is the user response area
[DisplayName("Answer: ")]
public string InformationIntakeValue { get; set; }
[DisplayName("Choice: ")]
public string InformationIntakeValueBool { get; set; }
[DisplayName("Date: ")]
public DateTime InformationIntakeValueDate { get; set; }
[ForeignKey("Survey")]
public int SurveyId { get; set; }
public virtual Survey Survey { get; set; }
}
(Note: fyi, I've tried the models without foreign keys as well - but perhaps it's not defined correctly)
The controller :
// GET: /Inquiry/Create
public ActionResult Create()
{
var geoIpData = Strings._download_serialized_json_data<GeoData>(StringConstants.UrlForGeoService);
SurveyModel = new Survey
{
Id=1,
Address = new AddressController().GetAddressModel(geoIpData.country, geoIpData.regionName, geoIpData.city, ""),
Questions = new QuestionController().GetQuestions(1).ToList()
};
return View(SurveyModel);
}
// POST: /Inquiry/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
public ActionResult Create([Bind(Include = "Id, Name, ContactMethodPhone, ContactMethodEmail, Address, Questions")] Survey survey)
{
if (ModelState.IsValid)
{
int i = 0;
}
if (survey.Address.Cities == null)
{
survey.Address.Cities = SurveyModel.Address.Cities;
survey.Address.Countries = SurveyModel.Address.Countries;
survey.Address.MailCodes = SurveyModel.Address.MailCodes;
survey.Address.Provinces = SurveyModel.Address.Provinces;
survey.Address.Tenements = SurveyModel.Address.Tenements;
}
if (survey.Questions == null)
{
survey.Questions = SurveyModel.Questions;
}
return View(survey);
}
The view:
#model BindCollection.Models.Survey
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
// I use <div class="container">, <fieldset> and <div class="row">
// instead of <div class="form-horizontal"> and <div class="form-group">
<div class="container">
<fieldset>
<legend></legend>
<h4>Survey</h4>
<hr />
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Id)
<div class="row">
#Html.LabelFor(model => model.Name, new { #class = "control-label col-md-2 col-md-2 col-lg-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
Your address information:<br />
<br />
</div>
</div>
#* no problem here with address *#
#{ var vddAddress = new ViewDataDictionary(ViewData) { TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = "Address" } };}
#Html.Partial("_AddressPartial", Model.Address, #vddAddress)
<hr />
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
How we can contact you? :<br />
<br />
</div>
</div>
<div class="row">
#Html.LabelFor(model => model.ContactMethodPhone, new { #class = "control-label col-sm-2 col-md-2 col-lg-2" })
<div class="col-sm-10 col-md-10 col-lg-10">
#Html.EditorFor(model => model.ContactMethodPhone)
#Html.ValidationMessageFor(model => model.ContactMethodPhone)
</div>
</div>
<div class="row">
#Html.LabelFor(model => model.ContactMethodEmail, new { #class = "control-label col-sm-2 col-md-2 col-lg-2" })
<div class="col-sm-10 col-md-10 col-lg-10">
#Html.EditorFor(model => model.ContactMethodEmail)
#Html.ValidationMessageFor(model => model.ContactMethodEmail)
</div>
</div>
<hr />
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
Some Questions<br />
<br />
</div>
</div>
#*Here is the evil one! Beware!*#
#{ var vddQuestions = new ViewDataDictionary(ViewData) { TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = "Questions" } };}
#Html.Partial("_QuestionsPartial", Model.Questions, #vddQuestions)
<hr />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</fieldset>
</div>
}
The address partial view is insignificant as there's no problem there.
Here's the partial view for the questions:
#model IEnumerable<BindCollection.Models.Question>
#using BindCollection
#using Kendo.Mvc.UI
#{
string CurrentTopic = string.Empty;
bool FirstTime = true;
}
#foreach (var item in Model)
{
if (CurrentTopic != item.InformationIntakeTopicValue)
{
CurrentTopic = item.InformationIntakeTopicValue;
if (!FirstTime)
{
FirstTime = false;
item.InformationIntakeTopicValue = string.Empty;
}
}
else
{
item.InformationIntakeTopicValue = string.Empty;
}
#Html.EditorFor(m=>item, "Question")
<br />
<br />
}
and, of course, I made an EditorTemplate for a question, as you can see a few lines above...
#model BindCollection.Models.Question
#Html.HiddenFor(m=>m.Id)
#{if (!string.IsNullOrWhiteSpace(Model.InformationIntakeTopicValue))
{
<hr />
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<br />
<br />
<h4>
#Html.DisplayFor(m => m.InformationIntakeTopicValue, new { #class = "control-label col-sm-10 col-md-10 col-lg-10" })
</h4>
<br />
</div>
</div>
}}
#*type of value to ask for is denoted by item.InformationIntakeTypeCode*#
<div class="row">
<div class="control-label col-sm-9 col-md-9 col-lg-9">
#Html.DisplayFor(m => m.InformationIntakeGroupValue, null)
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
#{
var outputBool = false;
var outputDate = false;
var outputText = false;
if(Model.InformationIntakeTypeCode.ToLower().Contains("date") )
{
outputDate = true;
}
else if (Model.InformationIntakeTypeCode.ToLower().Contains("bool"))
{
outputBool = true;
}
else if (Model.InformationIntakeTypeCode.ToLower().Contains("string"))
{
outputText = true;
}
}
#if(outputBool)
{
#(Html.Kendo().DropDownListFor(m => m.InformationIntakeValueBool)
.HtmlAttributes(new { style ="width: 100px;", id="InformationIntakeValueBool"+Model.Id.ToString() })
.DataTextField("Text")
.DataValueField("Value")
.BindTo(new List<SelectListItem>() {
new SelectListItem() {
Text = "Please Select...",
Value = "0"
},
new SelectListItem() {
Text = "Yes",
Value = "true"
},
new SelectListItem() {
Text = "No",
Value = "false"
}
}).Value("0")
)
}
#if(outputDate)
{
#(Html.Kendo().DatePickerFor(m => m.InformationIntakeValueDate)
.HtmlAttributes(new { style = "width: 100px;", id="InformationIntakeValueDate"+Model.Id.ToString() })
)
}
#if (outputText)
{
#Html.Kendo().AutoCompleteFor(m => m.InformationIntakeValue).HtmlAttributes(new { style = "width: 80%;", id="InformationIntakeValue"+Model.Id.ToString()})
}
</div>
</div>
So... When I POST, an odd thing occurs. All form values are passed, but the ones for Questions look strange:
Id 1
Name sally
Address.Street 123 Avenue Steet
Address.Street2 Building C
Address.Tenement Suite
Address.ApartmentNum 111
Address.City_input Sarnia
Address.City Sarnia
Address.State_input Ontario
Address.State Ontario
Address.Country_input Canada
Address.Country Canada
Address.MailCode_input N6B 2K0
Address.MailCode N6B 2K0
ContactMethodPhone 555-555-5555
ContactMethodEmail r#r.com
Questions.item.Id 1
Questions.item.InformationIntakeValueBool true
Questions.item.Id 2
Questions.item.InformationIntakeValueDate 2/4/2014
Questions.item.Id 3
Questions.item.InformationIntakeValue Speckled
Questions.item.Id 4
Questions.item.InformationIntakeValueBool true
Questions.item.Id 5
Questions.item.InformationIntakeValue Lightly Toasted
Questions.item.Id 7
Questions.item.InformationIntakeValueBool true
Questions.item.Id 8
Questions.item.InformationIntakeValue Nothing!
Questions.item.Id 6
Questions.item.InformationIntakeValueBool true
Questions.item.Id 9
Questions.item.InformationIntakeValueDate 6/29/2014
I thought, as I've seen in other posts, that the Question items should look like:
Questions[0].Id 1
Questions[0].InformationIntakeValueBool true
Questions[1].Id 2
Questions[1].InformationIntakeValueDate 2/4/2014
Questions[2].Id 3
Questions[2].InformationIntakeValue Speckled
So I'm not sure why mine looks like this.
On the server side, the Request only shows one variable for each:
Request.Form.Results View Expanding the Results View will enumerate the IEnumerable
[0] "__RequestVerificationToken" object {string}
[1] "Id" object {string}
[2] "Name" object {string}
[3] "Address.Street" object {string}
[4] "Address.Street2" object {string}
[5] "Address.Tenement" object {string}
[6] "Address.ApartmentNum" object {string}
[7] "Address.City_input" object {string}
[8] "Address.City" object {string}
[9] "Address.State_input" object {string}
[10] "Address.State" object {string}
[11] "Address.Country_input" object {string}
[12] "Address.Country" object {string}
[13] "Address.MailCode_input" object {string}
[14] "Address.MailCode" object {string}
[15] "ContactMethodPhone" object {string}
[16] "ContactMethodEmail" object {string}
[17] "Questions.item.Id" object {string}
[18] "Questions.item.InformationIntakeValueBool" object {string}
[19] "Questions.item.InformationIntakeValueDate" object {string}
[20] "Questions.item.InformationIntakeValue" object {string}
See the last 4 items? What about the other records?
I'm guessing that there's something strange with the ViewDataDictionary that I'm sending to the Question Partial View.
Any help would be appreciated. This should be simple...
Neither Partial Views or foreach statements contain the necessary information to properly bind collections. I always use EditorTemplates for everything. Especially since EditorTemplates will automatically iterate over a collection.
However, if you're bound and determined to use a loop, then use a for loop, and then index the model. In your case:
#for(int i = 0; i < Model.Count; i++)
{
if (CurrentTopic != Model[i].InformationIntakeTopicValue)
...
#Html.EditorFor(m => Model[i]) // don't have to specify the template name
// since it's the same name as the type
}
However, i'd just do this:
In your view, do this:
#*Here is the evil one! Beware!*#
#Html.EditorFor(m => m.Questions)
Then have your Question.cshtml as usual, which will automatically get iterated over by the EditorFor
However, at the top of the Question.cshtml (after the model declaration) add the following code, it's all that's necessary to achieve what you're trying to do. You don't need the partial view at all.
#{
if (ViewBag.CurrentTopic != Model.InformationIntakeTopicValue)
{
ViewBag.CurrentTopic = Model.InformationIntakeTopicValue;
}
else
{
Model.InformationIntakeTopicValue = string.Empty;
}
}
Related
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 = ""})
i have a problem about MVC, but first I am sorry for my english :D .
Now i am trying to make a form for users and i have a critical issue when i want connect to values with database.
My Form is like this : https://i.hizliresim.com/vJ6r2p.png
Models :
[Table("Testers")]
public class Testers
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[StringLength(50),Required]
public string testerName { get; set; }
public ICollection<Scores> Scores { get; set; }
}
[Table("Technologies")]
public class Technologies
{
[Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[StringLength(50)]
public string technologyName { get; set; }
[StringLength(50)]
public string type { get; set; }
}
[Table("Scores")]
public class Scores
{
[Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[DefaultValue(0)]
public int score { get; set; }
public virtual Testers tester { get; set; }
public virtual Technologies technology { get; set; }
}
ViewModels:
public class TechnologiesView
{
public List<Technologies> Technologies { get; set; }
public Scores Scores { get; set; }
}
Controller :
public ActionResult Page2()
{
TechnologiesView allTechs = new TechnologiesView();
allTechs.Technologies = db.Technologies.ToList();
return View(allTechs);
}
View:
#model TechnologiesView
#{
ViewBag.Title = "Page2";
}
<style>
#lang {
font-size: 15px;
color: gray;
}
#tech {
font-size: 13px;
color: gray;
}
</style>
<div class="container">
<div class="row col-xs-12 bilgi" style="color:black">
#HelperMethods.Title("Kendini Skorla!")
<br />
<i>Bilgi Düzeyini 0 ile 5 puan arasında notlar mısın? (0=Hiç 5= İleri Seviye)</i>
</div>
</div>
<hr />
#using (Html.BeginForm())
{
<div class="container-fluid" style="padding-left:50px; margin:0px">
<div class="row" id="lang">
#foreach (Technologies techs in Model.Technologies)
{
if (techs.type == "lang")
{
<div class="col-md-1 col-sm-2 col-xs-6">
#(techs.technologyName)
</div>
<div class="col-md-1 col-sm-2 col-xs-6">
(#(Html.TextBoxFor(x => x.Scores.score, new
{
id = techs.ID,
name = "techID",
style = "display:inline; width:20px; height:20px; font-size:smaller; padding:0px; text-align:center",
#class = "form-control"
})))
</div>
}
}
</div>
<hr style="color:black" />
<div class="row" id="tech">
#foreach (Technologies techs in Model.Technologies)
{
if (techs.type == "tech")
{
<div class="col-md-1 col-sm-2 col-xs-6" id="tech">
#(techs.technologyName)
</div>
<div class="col-md-1 col-sm-2 col-xs-6">
#Html.HiddenFor(x=>techs.ID)
(#(Html.TextBoxFor(x => x.Scores.score, new
{
id = techs.ID,
name = "techID",
style = "display:inline; width:20px; height:20px; font-size:smaller; padding:0px; text-align:center",
#class = "form-control"
})))
</div>
}
}
</div>
<hr />
<div class="row col-xs-12" id="lang">
<span>Kullandığınız IDE’ler (yazınız)</span>
<br />
<div style="margin-bottom:10px; text-align:center">
#HelperMethods.TextArea("Ide", 3)
</div>
</div>
<div style="text-align:right; margin-bottom:10px">
#HelperMethods.Button("btnPage2")
</div>
</div>
}
Now user has to give a score to him/herself for every technologies or languages and after this i want to when user click to button "Follow the next page(it's turkish)" i will select the last saved user from maxID value in Testers and i have to connect scores with technologies and testers but i don't know how can i get textboxes' values and which technology's value is this value on post :D
You generating form controls which have no relationship at all to your model (which is also wrong anyway). Never attempt to change the name attribute when using the HtmlHelper methods (and there is no reason to change the id attribute either)
Next, you cannot use a foreach loop to generate form controls for a collection. You need a for loop or EditorTemplate to generate the correct name attributes with indexers. Refer this answer for a detailed explanation.
Then you cannot use a if block inside the loop (unless you include a hidden input for the collection indexer), because by default the DefaultModelBinder required collection indexers to start at zero and be consecutive.
First start by creating view models to represent what your want to display/edit in the view.
public class ScoreVM
{
public int ID { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
public class TechnologiesVM
{
public List<ScoreVM> Languages { get; set; }
public List<ScoreVM> Technologies { get; set; }
public string Notes { get; set; } // for your textarea control
}
Note you will probably want to add validation attributes such as a [Range] attribute for the Score property
In the GET method, initialize and populate your view model and pass it to the view
public ActionResult Page2()
{
IEnumerable<Technologies> technologies = db.Technologies;
TechnologiesVM model = new TechnologiesVM
{
Languages = technologies.Where(x => x.type == "lang")
.Select(x => new ScoreVM{ ID = x.ID, Name = x.technologyName }).ToList(),
Technologies = technologies.Where(x => x.type == "tech")
.Select(x => new ScoreVM{ ID = x.ID, Name = x.technologyName }).ToList(),
};
return View(model);
}
and in the view
#model TechnologiesVM
....
#using (Html.BeginForm())
{
....
#for (int i = 0; i < Model.Languages.Count; i++)
{
#Html.HiddenFor(m => m.Languages[i].ID)
#Html.HiddenFor(m => m.Languages[i].Name)
#Html.LabelFor(m => m.Languages[i].Score, Model.Languages[i].Name)
#Html.TextBoxFor(m => m.Languages[i].Score)
#Html.ValidationMessageFor(m => m.Languages[i].Score)
}
#for (int i = 0; i < Model.Languages.Count; i++)
{
.... // repeat above
}
#Html.LabelFor(m => m.Notes)
#Html.TextAreaFor(m => m.Notes)
#Html.ValidationMessageFor(m => m.Notes)
<input type="submit" />
}
and the POST method will be
public ActionResult Page2(TechnologiesVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
... // save the data and redirect
}
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.
I am trying to create an invoice. I have two models, Invoice & InvoiceItems.
I am able to insert using hardcoded values, but I want to be able to use TextBoxes to create an invoice on the fly. How do I insert a record that takes the data for the invoice and the dynamic data from the invoice items and inserts into both tables, using the same view? I'd like to have an add more button eventually where I can stay on the same page and keep adding items to the same invoice. You can see what I've tried so far below.
Invoice Model:
public class Invoice
{
[Key]
public int InvoiceId { get; set; }
public int ClientId { get; set; }
[Display(Name = "Amount")]
public decimal Amount { get; set; }
[Display(Name = "Invoice Creation Date")]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
public DateTime CreationDate { get; set; }
[Display(Name = "Invoice Due Date")]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
public DateTime DueDate { get; set; }
[Display(Name = "Notes")]
public string InvoiceNotes { get; set; }
public List<InvoiceDetails> InvoiceDetails { get; set; }
public List<Clients> Clients { get; set; }
}
InvoiceItem Model:
public class InvoiceDetails
{
[Key]
public int InvoiceDetailsId { get; set; }
public int InvoiceId { get; set; }
[DisplayName("Item Name")]
public string Name { get; set; }
[DisplayName("Item Note")]
public string Note { get; set; }
[DisplayName("Qty")]
public decimal? Quantity { get; set; }
[DisplayName("Rate/Hour")]
public decimal? Price { get; set; }
[DisplayName("Item Total")]
public decimal? Total { get; set; }
}
Invoice Controller:
private NovaDb _db = new NovaDb();
public ActionResult InvoiceInformation()
{
var invoice = new Invoice();
invoice.InvoiceDetails = new List<InvoiceDetails>();
return View(invoice);
}
[HttpPost]
public ActionResult InvoiceInformation(Invoice model)
{
if (ModelState.IsValid)
{
var invoices = new Invoice()
{
Amount = model.Amount,
CreationDate = model.CreationDate,
DueDate = model.DueDate,
InvoiceNotes = model.InvoiceNotes,
InvoiceId = model.InvoiceId,
ClientId = model.ClientId
};
_db.Invoices.Add(invoices);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(model);
}
Invoice View:
#model NovaFinancial.Models.Invoice
#{
ViewBag.Title = "InvoiceInformation";
}
<h2>InvoiceInformation</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Invoice</legend>
#Html.HiddenFor(model => model.InvoiceId)
<div class="editor-label">
#Html.LabelFor(model => model.ClientId)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.ClientId)
#Html.ValidationMessageFor(model => model.ClientId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Amount)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Amount)
#Html.ValidationMessageFor(model => model.Amount)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.CreationDate)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.CreationDate)
#Html.ValidationMessageFor(model => model.CreationDate)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DueDate)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.DueDate)
#Html.ValidationMessageFor(model => model.DueDate)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.InvoiceNotes)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.InvoiceNotes)
#Html.ValidationMessageFor(model => model.InvoiceNotes)
</div>
<div>
<table>
<tr>
<th>Name</th>
<th>Notes</th>
<th>Qty</th>
<th>Price</th>
<th>Total</th>
</tr>
#for (int i = 0; i < Model.InvoiceDetails.Count; i++)
{
#Html.HiddenFor(m=>m.InvoiceDetails[i].Name)
#Html.HiddenFor(m=>m.InvoiceDetails[i].Note)
#Html.HiddenFor(m=>m.InvoiceDetails[i].Quantity)
#Html.HiddenFor(m=>m.InvoiceDetails[i].Price)
#Html.HiddenFor(m=>m.InvoiceDetails[i].Total)
<tr>
<td>#Html.DisplayFor(m=>m.InvoiceDetails[i].Name) | #Html.TextBoxFor(m=>m.InvoiceDetails[i].Name)</td>
<td>#Html.DisplayFor(m=>m.InvoiceDetails[i].Note) | #Html.TextBoxFor(m=>m.InvoiceDetails[i].Note)</td>
<td>#Html.DisplayFor(m=>m.InvoiceDetails[i].Quantity) | #Html.TextBoxFor(m=>m.InvoiceDetails[i].Quantity)</td>
<td>#Html.DisplayFor(m=>m.InvoiceDetails[i].Price) | #Html.TextBoxFor(m=>m.InvoiceDetails[i].Price)</td>
<td>#Html.DisplayFor(m=>m.InvoiceDetails[i].Total) | #Html.TextBoxFor(m=>m.InvoiceDetails[i].Total)</td>
</tr>
}
</table>
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
I worked out "a" solution that might not be the best, but it works. You'd still have to add to the code to make it more robust, but the general framework is there.
In your view, keep all the text boxes for the invoice details (Name, Quantity, Price) the same value for the name attribute, but do keep the id's unique. I used a bit of jQuery and JavaScript to generate extra rows as needed per a button that the user would click. For example,
<input type="text" name="Name" id="Name"> <!--first line item for Item Name-->
<input type="number" name="Quantity" id="Quantity"> <!--first for Quanitity -->
<input type="text" name="Name" id="Name2"> <!--second line item for Item Name-->
<input type="number" name="Quantity" id="Quantity2"> <!-- second for Quanitity -->
The values for the InvoiceDetail lines will pass back to the server as comma-delimited strings (better make certain that your item names don't have commas!). On the server-side,
var Names = Request["Name"]; // this would yield something like "Labor,Parts"
In the controller, you'll need to parse the strings into arrays and create the instances of your InvoiceDetail from them. I wrote a private method to split the strings and return a list of InvoiceDetail objects to the action method. The onus is on you to validate this data: both client-side and server-side need validation.
I did a few experiments. You can see all the code here: http://mefixme.blogspot.com/2014/10/aspnet-mvc-how-to-add-model-with.html
I hope that this helps you.
This is a continuation of this question Model class and Mapping
I had my Client class now working fine and it's defined as
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using DataAnnotationsExtensions;
using System.ComponentModel.DataAnnotations.Schema;
using System.Collections.Generic;
namespace CardNumbers.Objects
{
[ComplexType]
public class PhoneInfo
{
[DataType(DataType.PhoneNumber)]
[StringLength(10)]
[DisplayName("Phone")]
public virtual string Phone { get; set; }
[StringLength(5)]
[DisplayName("Ext")]
public virtual string Ext { get; set; }
public bool HasValue
{
get
{
return (Phone != null || Ext != null);
}
}
}
[ComplexType]
public class ContactDetail
{
//Constructor
public ContactDetail()
{
phoneInfo = new PhoneInfo();
}
[StringLength(100)]
[DisplayName("Contact Name")]
[DisplayFormat(NullDisplayText = "")]
public virtual string Contact { get; set; }
[Email]
[StringLength(100)]
[DisplayName("Email")]
public virtual string Email { get; set; }
public virtual PhoneInfo phoneInfo { get; set; }
public bool HasValue
{
get
{
return (Contact != null || Email != null || phoneInfo.HasValue);
}
}
}
/// <summary>
/// Client class (Client No, Client Name, Address, Contact1, Contact2 info, Created By, Modified By (operator and date)
/// </summary>
public class Client
{
public Client()
{
Contact1 = new ContactDetail();
Contact2 = new ContactDetail();
}
[Key]
[Column("ClientId",TypeName = "int")]
public virtual int Id { get; set; }
[Required]
[DisplayName("Client No")]
[Column("client_no", TypeName = "smallint")]
public virtual Int16 Number { get; set; }
[Required]
[Column("client_name", TypeName = "varchar")]
[DisplayName("Client Name")]
[MaxLength(30, ErrorMessage = "Client Name should not be longer than 30 characters" )]
[MinLength(3, ErrorMessage = "Client Name is too short")]
public virtual string Name { get; set; }
[DataType(DataType.MultilineText)]
public virtual string Address { get; set; }
public virtual ContactDetail Contact1 {get; set;}
public virtual ContactDetail Contact2 {get; set;}
[ForeignKey("EnteredByOperator")]
public string EnteredBy { get; set; }
[InverseProperty("ClientsEnteredBy")]
public virtual Operator EnteredByOperator { get; set; }
[ForeignKey("ModifiedByOperator")]
public string ModifiedBy { get; set; }
[InverseProperty("ClientsUpdatedBy")]
public virtual Operator ModifiedByOperator { get; set; }
[DataType(DataType.DateTime)]
[DisplayName("Created on")]
public DateTime EnteredOn { get; set; }
[DataType(DataType.DateTime)]
[DisplayName("Modified on")]
public DateTime? ModifiedOn { get; set; }
public virtual ICollection<ClientOrder> ClientOrders { get; set; }
public virtual ICollection<Reorder> Reorders { get; set; }
}
}
I mapped column names using Fluent API and I also re-defined my original "repository" classes to be very similar to defined in this tutorial http://code.msdn.microsoft.com/ASPNET-MVC-Application-b01a9fe8
This is my current partial view for the client form called _ClientForm:
#using WebDemo.Helper
#model CardNumbers.Objects.Client
<fieldset>
<legend>Client Info</legend>
#Html.ValidationSummary(true)
<input type="hidden" id="fntype" name="fntype">
#Html.HiddenFor(model => model.Id)
#Html.EditorFor(model => model.Number, EditorTemplate.TextBox)
#Html.EditorFor(model => model.Name, EditorTemplate.TextBox)
#Html.EditorFor(model => model.Address, EditorTemplate.EditBox)
<div id="ContactsInfo">
#*Contact 1*#
<div id="Contact1">
#*#Html.EditorFor(model=>model.Contact1)*#
#Html.EditorFor(model=>model.Contact1.Contact, EditorTemplate.TextBox)
#Html.EditorFor(model=>model.Contact1.Email, EditorTemplate.TextBox)
</div>
#*Contact2*#
<div id="Contact2">
#* #Html.EditorFor(model => model.Contact2)*#
</div>
</div>
#*<div class="clear"></div>*#
<div id="SaveCancel" class="float-right">
<button type="Submit" id="btnSave">Save</button>
<button type="reset" id="btnCancel">Cancel</button>
</div>
</fieldset>
I already tried to revert to original way of only one level and I also commented the second Contact2 info but still the e-mail validation doesn't work and all other validations also don't seem to work.
The EditorFor textboxes are defined based on this blog post http://fusionovation.com/post/2010/02/15/adding-a-rich-text-editor-to-asp-net-mvc-using-strongly-typed-helpers-dataannotations-amp-jquery.aspx
And these are two of the new EditorFor I added:
PhoneInfo.cshtml
#using WebDemo.Helper
#model CardNumbers.Objects.PhoneInfo
<div id="PhoneInfo">
<div class="float-left">
#Html.EditorFor(model => model.Phone, EditorTemplate.TextBox)
</div>
<div class="float-right">
#Html.EditorFor(model => model.Ext, EditorTemplate.TextBox)
</div>
</div>
And ContactDetail.cshtml
#using WebDemo.Helper
#model CardNumbers.Objects.ContactDetail
#Html.EditorFor(model => model.Contact, EditorTemplate.TextBox)
#Html.EditorFor(model => model.Email, EditorTemplate.TextBox)
#Html.EditorFor(model=>model.phoneInfo)
So, as you can see, the code of the views is now very compact.
However, with all of these in place the validations don't see to fire anymore. I used to test validation on EMail by typing some garbage. It used to provide a validation message near the textbox. Now I observe that the email textbox takes the red border, but there is no message.
Do you see what I am missing now and if it's possible to use complex type and validations?
To clarify, the _ClientForm is called from this Client view:
#model CardNumbers.Objects.Client
#{
ViewBag.Title = "Client";
}
#section scripts {
<script src="#Url.Content("~/Scripts/Clients.js")" type="text/javascript" ></script>
}
<form id="frmClientsSearch">
<label for="clientNo">Client No: </label>
<input type="number" name="searchClientNo" class="numericOnly" /><br />
<label for="clientName">Client Name: </label>
<input type = "text" size =25 value ="Please enter the search value" class="SelectOnEntry"
name ="searchClientName" />
<input type="button" id="btnClientsSearch" value ="Find / Refresh" />
</form>
<div style="padding-left: 150px; padding-top: 50px; padding-bottom: 50px;" id="ClientsResults">
<table id="flexClients" style="display: none">
</table>
</div>
<div style="display: none">
<form id="sform" title="Client Info">
#{Html.RenderPartial("_ClientForm", Model) ;}
</form>
</div>
Thanks.
I don't see a form anywhere on your page. A form context is required for validation to work. You need to wrap the Editor attributes in BeginForm block.
After some trials and error I found that the TextBox EditorFor view was the culprit. I documented what I found in my answer here http://forums.asp.net/t/1855963.aspx/1?Validation+messages+don+t+show+up+what+is+missing+
Basically, as long as I use this EditorFor
#*#using WebDemo.Helper*#
#model CardNumbers.Objects.PhoneInfo
<div id="PhoneInfo">
<div class="float-left">
#* #Html.EditorFor(model => model.Phone, EditorTemplate.TextBox)*#
<div class="editor-label">
#Html.LabelFor(model => model.Phone)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Phone)
#Html.ValidationMessageFor(model => model.Phone)
</div>
</div>
<div class="float-right">
#*#Html.EditorFor(model => model.Ext, EditorTemplate.TextBox)*#
<div class="editor-label">
#Html.LabelFor(model => model.Ext)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Ext)
#Html.ValidationMessageFor(model => model.Ext)
</div>
</div>
</div>
All seems to work OK. But if I try to switch to a shorter syntax and use this EditorFor for the textbox:
<div class="editor-label">
#Html.Label((ViewData.ModelMetadata.DisplayName??ViewData.ModelMetadata.PropertyName),
new Dictionary<string, object>
{
{ "for", ViewData.ModelMetadata.PropertyName }
})
</div>
<div class="editor-field">
#Html.TextBox("", (object)Model,
new Dictionary<string, object>
{
{ "id", ViewData.ModelMetadata.PropertyName },
{ "name", ViewData.ModelMetadata.PropertyName },
{ "class", "text-box single-line"},
{ "data-bind", "value: " + ViewData.ModelMetadata.PropertyName },
})
#Html.ValidationMessage(ViewData.ModelMetadata.PropertyName,
new Dictionary<string, object>
{
{ "data-valmsg-for", ViewData.ModelMetadata.PropertyName }
})
</div>
Validation messages do not show anymore.
Hopefully this answer will help someone or you may see what I am missing here.