Help with asp.net mvc nested model binding - asp.net-mvc

So I have this demo project almost completely working.
public class Project
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Task> Tasks { get; set; }
}
public class Task
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
public int ProjectID { get; set; }
public virtual Project Project { get; set; }
}
Controller
public ActionResult Edit(int id)
{
var project = db.Projects.Where(p=>p.ID==id).Single();
return View(project);
}
[HttpPost]
public ActionResult Edit(Project project)
{
if (ModelState.IsValid)
{
var dbProject = db.Projects.Where(p => p.ID == project.ID).Single();
UpdateModel(dbProject);
db.SaveChanges();
TempData["Success"] = "Modelo Valido";
}
return RedirectToAction("Index");
}
View//strongly typed for project
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Project</legend>
#Html.HiddenFor(model => model.ID)
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<h1>Tasks</h1>
#Html.EditorFor(m => m.Tasks)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
EditorTemplate
#model MvcApplication2.Models.Task
<span>Task</span>
<br />
#Html.LabelFor(m => m.Name)
#Html.EditorFor(m => m.Name)
#Html.HiddenFor(m => m.ID)
#Html.HiddenFor(m => m.ProjectID)
#Html.ValidationMessageFor(m => m.Name)
The view displays this
The problem is that when I submit the form the Tasks are populated with everything except the virtual Project property... so the error i get it is
The operation failed: The relationship could not be changed because
one or more of the foreign-key properties is non-nullable. When a
change is made to a relationship, the related foreign-key property is
set to a null value. If the foreign-key does not support null values,
a new relationship must be defined, the foreign-key property must be
assigned another non-null value, or the unrelated object must be
deleted.
Here is a pic of my debugging breakpoint result
Please Help.
UPDATE:
I have changed my controller action to this
[HttpPost]
public ActionResult Edit(Project project)
{
if (ModelState.IsValid)
{
db.Entry(project).State = EntityState.Modified;
db.SaveChanges();
TempData["Success"] = "Modelo Valido";
return RedirectToAction("Index");
}
return View(project);
}
it is still not working correctly.
Now changes made to the Name of the project are updated correctly in the database. but changes made to any Task Name are ignored completely.

i believe #Html.EditorFor(m => m.Tasks) is generating html like (approximately)
<label>Name</label>
<input type="text" name="Tasks[0].Name" id="auto-gen-id"/>
<input type="hidden" name="Tasks[0].ID" id = "auto-gen-id"/>
<input type="hidden" name="Tasks[0].ProjectID" id = "auto-gen-id"/>
<!--html for validation span-->
Above is the approximate html generated for first Task in Collection and similar html will be generated for each task in the collection. The only difference is that index will be incremented in name attributes of all inputs i.e Tasks[1].Name, Tasks[1].ProjectID etc. This portion will actually bind to the Collection<Task> Tasks property of Project but you can see that in detail portion you don't have any inputs like
<input type="whatever" name="Tasks[0].Project.ProjectID" .../>
<input type="whatever" name="Tasks[0].Project.Name" ..../>
Modelbinder needs input elements with proper naming conventions to bind values to all properties of action method parameters. For testing purpose you can inlude these two lines in your Editor template for Task
#Html.TextBoxFor(x=>x.Project.ID)
#Html.TextBoxFor(x=>x.Project.Name)
input proper values for them in the form and you will have Project property of Task populated with these values. But may not be what you desire i.e entering project information twice and this may not be needed (if u are using Linq to sql its sure not needed). When you call your ORM for attaching entities to db entities it will know which Project elements, current Task belongs to.
Side Note: When you have problems with modelbinding, always pay attention to generated html. Generated html will dictate which form values will map to which properties of the model as long as you are using default modelbinder. it becomes especially important if you are having master detail kind of scenario as in your example.

I have found a way to get this to work but Im not completely happy with the approach.
see this question on how to refactor my current code to see how I am currently (hopefully temporarily doing it)
Help improving (refactoring) my code. Automapper - EF - asp.net mvc-3

Related

List Binding with model data

So I have a form that I am trying to submit and I can get either the list or the model to bind, but not both at the same time. I suspect it has to do with the model binder.
HTML
#using (Html.BeginForm("Index", "Home", FormMethod.Post)){
#Html.AntiForgeryToken()
<div class="TransferHeader">
<div>
#Html.LabelFor(model => model.tranRequestedBy)
#Html.EditorFor(model => model.tranRequestedBy, new { #Name = "h.tranRequestedBy" })
</div>
<div>
#Html.LabelFor(model => model.tranNotes)
#Html.EditorFor(model => model.tranNotes, new { #Name = "h.tranNotes" })
</div>
<input name="h.TransfersDetail.Index" id="detIndex" type="hidden" value="c3a3f7dd-41bb-4b95-b2a6-ab5125868adb">
<input name="h.TransfersDetail[c3a3f7dd-41bb-4b95-b2a6-ab5125868adb].detToolCode" id="detToolCode" type="hidden" value="1234">
</div>
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(TransfersHeader h)
{
return View();
}
Model Class:
public virtual ICollection<TransfersDetail> TransfersDetail { get; set; }
public string tranRequestedBy { get; set; }
public string tranNotes { get; set; }
The two bottom inputs were generated from an AJAX call to an add method, what happens is if they are not present the two HTML helper editors will come in the model, but if they do exist only the transfer detail list will appear.
Is there anything I could do to make sure all of the data comes into the model?
Its not clear how you are generating those inputs, but the name attributes are incorrect. You model does not contain a collection property named h, but it does contain one named TransfersDetail, so your inputs need to be
<input name="TransfersDetail.Index" type="hidden" value="c3a3f7dd-41bb-4b95-b2a6-ab5125868adb">
<input name="TransfersDetail[c3a3f7dd-41bb-4b95-b2a6-ab5125868adb].detToolCode" type="hidden" value="1234">
Its also not clear why your adding an id attribute (if you referencing collection items in jQuery, you would be better off using class names and relative selectors), but the id your using does not have an indexer suggesting that your going to be generating duplicate id attributes which is invalid html (and jQuery selectors would not work in any case)

ASP.NET MVC Details view with many display fields but a single editable field

I have a view that shows a single item with all of its fields. But I'm getting confused trying to figure out how to allow one specific field ("Status") to be updated from this view, without necessarily going into a whole other "edit" view first.
How do I http-post to the same action (Details) but saving only the "Status" field, without saving all of the other properties which are part of the view model (for display only)? Do I need a separate view model just for the Status? New to ASP.NET MVC and getting confused.
View Model
public string FirstName { get; set; }
public string LastName { get; set; }
public string Birthdate{ get; set; }
public string Status { get; set; }
//etc.
View
<div>
#Html.DisplayFor(model => model.FirstName)
#Html.DisplayFor(model => model.LastName)
#Html.DisplayFor(model => model.Birthdate)
//etc.
</div>
<div>
#TextBoxFor(model => model.Status)
<button type="submit" value="Save Status" />
</div>
Controller
[HttpGet]
public ActionResult Details(int id)
{
var person = personRepo.GetById(id);
var vm = BuildPersonDetailsViewModel(person);
return View(vm);
}
[HttpPost]
public ActionResult Details(PersonDetailsViewModel vm)
{
var person = personRepo.GetById(vm.PersonID);
person.Status = vm.Status;
personRepo.Update(person);
}
So I solved this by ensuring that the primary key field PersonID is included in the View. I didn't think I needed it originally because it started off as a read-only Details view, and PersonID wasn't needed to be displayed. But when posting data back, I needed to add it as a hidden field, so it could be passed to the controller on HttpPost. Then it can be used to locate and update the record in the database.
Furthermore, I added another method in the repository to allow for updating of just the "Status" field, since that's the only value being updated. If I use the sample code above, my solution looks something like this:
View Model
public int PersonID { get; set; }
public string Status { get; set; }
public string FirstName { get; set; }
//etc.
View
#Html.HiddenFor(model => model.PersonID)
#Html.DisplayFor(model => model.FirstName)
//etc. (all the display fields)
#TextBoxFor(model => model.Status)
<button type="submit" value="Save Status" />
Controller
[HttpPost]
public ActionResult Details(PersonDetailsViewModel vm)
{
personRepo.UpdateStatus(vm.PersonID, vm.Status);
}
This is probably not the case but since i can't tell from the sample code, do you have the Html.BeginForm(){} block around the model that you are trying to post? Also maybe try changing the <button> tag to <input type='submit' value='save status'/> instead

MVC Razor Form submit not working

I am trying to use a simple form to allow authorized users to modify content on select pages on an MVC3 Razor site that I'm building. I am unable to get the edit form to post correctly though.
My model is as follows:
public class WebContent
{
public virtual UInt32 id { get; set; }
public virtual String page { get; set; }
public virtual String section { get; set; }
[UIHint("tinymce_jquery_full"), AllowHtml]
public virtual String content { get; set; }
}
My Controller:
[Authorize]
public ActionResult Edit(String page, String section)
{
WebContent content = _WebContent.GetSection(page,section);
return View(content);
}
[Authorize]
[HttpPost]
public ActionResult Edit(WebContent content)
{
if (ModelState.IsValid)
{
_WebContent.Update(content);
return View("Index");
}
else return View("Index");
}
And my View:
#model SongbirdsStudios.Models.WebContent
#{
ViewBag.Title = "Edit '"+Model.page+"'Page Content";
}
<div>
<h2>Edit</h2>
#using (Html.BeginForm())
{
<fieldset>
<legend>Page Content</legend>
#Html.HiddenFor(m => m.id)
#Html.HiddenFor(m => m.page)
#Html.HiddenFor(m => m.section)
<div class="editor-label">
Content
</div>
<div class="editor-field">
#Html.EditorFor(m => m.content)
</div>
<p>
<input type="submit" value="Update" />
</p>
</fieldset>
}
</div>
The view renders correctly and displays the expected elements. The UIHint("tinymce_jquery_full") is getting picked up correctly and the TinyMCE editor appears on the page. But, when the form submits, I get an exception.
System.Web.HttpRequestValidationException: A potentially dangerous Request.Form value was detected from the client (content=...)
Everything I've read indicates that the AllowHTML attribute should allow this to post, but it's not for some reason.
I can get around this by adding the [ValidateInput(false)] attribute to the HttpPost controller method. If I do that, then this exception does not occur, but the model still does not get passed to the controller. It just passes null instead. Examining the HttpContext in the debugger indicates that it is passing 4 separate values - one for each property in my model instead of passing the model class back to the controller. I can't figure out what I need to change to make this work correctly.
I'm hoping it's something simple that I missed, and someone with a better eye can see what it is.
So after further investigation into how ASP MVC maps form fields to the model class and examining the HTML emitted to the browser, I found that this was an issue with the name of the property in my WebContent class.
public virtual String content { get; set; }
The TinyMCE editor uses a content variable to define certain characteristics associated with the editor interface. This was apparently causing the HTML 'content' generated by the user input in the editor to not get mapped back to the Model property.
Simply changing the name of the property in the model class (and of course fixing the corresponding database mapping and view references) immediately fixed the problem.
public virtual String web_data_content { get; set; }
Everything else being identical, this worked perfectly with the UIHint and AllowHTML attributes.
Add this attribute on your action
[ValidateInput(false)]
This should solve your problem
if you use ie7
this may has some err
<input type="submit" value="Update" />
give the button a name

How to turn off MVC form validation

I have a data-first set-up so my models are generated by the entity framework from my database and there is no default [Required] annotations. I have a simple table with three fields. One ID and two VARCHAR / text based fields.
No matter what I try, I cannot get the CRUD forms to stop validation. I disabled in the Web.config, I add [ValidateInput(false)] to the Create() method in the controller, but has no effect. I set the #Html.ValidationSummary to false,
This is the basic view:
#using (Html.BeginForm()) {
#Html.ValidationSummary(false)
<fieldset>
<legend>CallType</legend>
<div class="editor-label">
#Html.LabelFor(model => model.CALLTYPE)
</div>
<div class="editor-field">
#Html.TextBox("calltype", "", new { style = "width: 50px;" })
#Html.ValidationMessageFor(model => model.CALLTYPE)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DESCRIPTION)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.DESCRIPTION)
#Html.ValidationMessageFor(model => model.DESCRIPTION)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Model (generated by Framework):
public partial class CALLTYPES2
{
public int ID { get; set; }
public string CALLTYPE { get; set; }
public string DESCRIPTION { get; set; }
}
Even if I insert just one character in each field, it still says: "The Value 'x' is invalid"
(I leave the validation messages on so I can see what is going on.)
What am I supposed to do? And how would I validate these fields later on - can I just add [Required] to Model generated code? What if I regenerate the Model from the database?
Does this have something to do with the model state in the controller?
[HttpPost]
public ActionResult Create(CALLTYPES2 calltype)
{
if (ModelState.IsValid)
{
db.CALLTYPES2.Add(calltype);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(calltype);
}
Not sure what I am missing and the tutorials I have read do not shed much light. Thanks for your response and apologies for my ignorance.
UPDATE
Found my error - The object name "calltype" in the Method Create() is the same as the name/id of the form field "calltype". I guess the binder tries to bind the string "calltype" to the object "calltype". Renamed it to:
public ActionResult Create(CALLTYPES2 ctype)
Now it works in both the Edit and Create Windows. "ctype" is not clashing with "calltype".
You forgot to include the ID field in your form. You could include it as a hidden field:
#Html.HiddenFor(model => model.ID)
Now the value of the ID property will be sent to the server when the form is submitted and the default model binder should not complain.

MVC Binding Data

I have a MVC Project.
I have questions in a database that I want my users to answer. I have them in the database because they need to be able to be dynamic. The user needs to submit an answer for each question. So when the view displays, it shows the question and a textbox for each question.
What would the best way to go about doing this as I do not want to hard code in the question like #Html.textBox("Question1") etc.
Question 1 [__textbox1___]
Question 2 [__textbox2___]
... etc.
I'd probably have a ViewModel that contains a collection of a custom QuestionResponder type.
IEnumerable<IQuestionResponder> Questions{get;set;}
public interface IQuestionResponder{
Guid QuestionId{get;set;}
string Question{get;set;}
string Answer{get;set;}
}
Then you can create the display items you require for your new view model.
#for(var i = 0; i <= questionList .Count; i++)
{
#Model.Questions.ToList()[i].Question
#Html.TextBoxFor(m => m.Questions.ToList()[i].Answer)
}
Alternartively you can create an EditorTemplate to avoid looping in your view:
Insdie ~/Views/Shared/EditorTemplates/ add a new view called QuestionResponder (the name of your custom class).
Inside that template you can then add:
#model MyApp.Models.QuestionResponder
<div>
#Html.DisplayFor(m => m.Question)
#Html.TextBoxFor(m => m.Answer)
</div>
While you'll then call from your original view:
#Html.EditorFor(m => m.Questions)
There's builtin way of doing that in MVC. Very simple way, by the way. There are many alternatives, take a look at this article by Phil Haack to inspect them all. One of those is with dictionaries. Quesion.Id will be key, Answer will be value
Sample:
First, create appropriate ViewModels
public class AnswerQuestionViewModel
{
public Quesion Question { get; set; }
public string Answer { get; set; }
}
public class Quesion //
{
public int Id { get; set; }
// Maybe some other properties.
}
Inside ~/Views/Shared/EditorTemplates/, create editor that will render Editor.
#model Models.AnswerQuestionViewModel
#Html.HiddenFor(model => model.Question.Id)
#Html.EditorFor(model => model.Answer)
And ~/Views/ControllerName/ActionName.cshtml
#model IEnumerable<ControllerInspectorTest.Models.AnswerQuestionViewModel>
#using (Html.BeginForm())
{
#Html.EditorForModel();
<p>
<input type="submit" value="Create" />
</p>
}
And when you create post action, parameter will be filled in
[HttpPost]
public ActionResult AnswerQuestions(IEnumerable<AnswerQuestionViewModel> quesions)
{
// questions parameter is filled in correctly
//do save job;
}
Note that question parameter can by type of IList or List too

Resources