MVC Binding complex objects with list of values - c#

I have looked at most of the available help in SO and google related to this but I'm not sure what I'm doing wrong.
I have Model with some properties, and one of the properties is a list of another complex object. I'm not able to bind this list!
Pleas help!
Here is my Model classes related:
public class PrivacyModel
{
public int RatingId { get; set; }
public List<RatingPoint> RatingPoints { get; set; }
}
public class RatingPoint
{
public int RatingItemId { get; set; }
public string RatingValue { get; set; }
}
Here is my code:
[HttpPost]
public ActionResult Index(PrivacyModel model)
{
.... my business logic....
}
My view looks like this:
#using (Html.BeginForm("Index", "Privacy"))
{
<input type="hidden" name="RatingId" value="#Model.RatingId" />
for (var i = 0; i < Model.RatingPoints.Count; i++)
{
var ratingPoint = Model.RatingPoints[i];
<input type="hidden" name="PrivacyModel.RatingPoints[#i].RatingItemId" value="#ratingPoint.RatingItemId" />
<input type="hidden" name="PrivacyModel.RatingPoints[#i].RatingValue" #("id=RatingPoints" + ratingPoint.RatingItemId) value="#ratingPoint.RatingValue" />
}
<input class="btn" type="submit" value="Submit" />
}
Please don't mind the value and id fields, they are being updated by jQuery somewhere in my page correctly.
This got me a null list of RatingPoints in my action
I have tried also without the prefix PrivacyModel in PrivacyModel.RatingPoints[#i]., but this got me an empty list of RatingPoints in my action
I have also tried using an Index like in the suggested solution here for non-sequential items

You are making it complex yourself, you can just use HiddenFor() helper for this:
for (var i = 0; i < Model.RatingPoints.Count; i++)
{
#Html.HiddenFor(x=> Model.RatingPoints[i].RatingItemId)
#Html.HiddenFor(x=> Model.RatingPoints[i].RatingValue,new { id= "RatingPoints"+Model.RatingPoints[i].RatingItemId})
}
and this will render the same html, and values will be correctly binded in Model at post.

Related

what should be parameters of action method when there is foreach loop in your form in view mvc

I am working on a project "Online examination system". User can give MCQ test here.
Now in my database I have a table named "Question" which contains questions of every subject in the database. When the user selects a subject for an exam, the questions of that subject are stored in var a by where query. Now that var a is passed to return view.
Then in the Exam view, the name of every field is dynamic and differs by ID column in table which is auto increment primary key.
Every MCQ is submitted to Exam_Result() Action method for Checking that its right or wrong.
Now the confusion is in Exam_Result() Action method's parameters... say 20 questions are submitted to Exam_Result, andn how should I handle dynamic names in the parameter of Exam_Result() ?
Exam() Action method in home controller:
public ActionResult Exam(string sname)
{
ProjectDatabaseEntities7 obj = new ProjectDatabaseEntities7();
Question q = new Question();
try
{
var a = obj.Questions.Where(s=>s.Subject_Name.Equals(sname));
return View(a);
}
catch
{
}
return View();
}
Exam.cshtml View:
<form action="/User/Exam_Result">
#foreach (var s in Model)
{
<div class="col-md-12 well form-group">
<label>#s.Question1</label><br>
<input type="text" name="question{#s.Id}" value="#s.Question1" hidden/>
<input type="radio" name="option{#s.Id}">#s.Option1<br />
<input type="radio" name="option{#s.Id}">#s.Option2<br />
<input type="radio" name="option{#s.Id}">#s.Option3<br />
<input type="radio" name="option{#s.Id}">#s.Option4<br />
</div>
}
<input type="submit" value="Submit" class="btn btn-success btn-group-justified" />
</form>
Exam_result() Action Method where the form above is submitting:
public ActionResult Exam_Result(string question, string option)
{
ProjectDatabaseEntities7 obj = new ProjectDatabaseEntities7();
Question q = new Question();
try
{
q = obj.Questions.First(x=> x.Question1.Equals(question));
if(q!=null)
{
if (q.Answer == option)
ViewBag.ans = "right ans";
else
ViewBag.ans = "wrong ans";
}
}
catch
{
return View();
}
return View();
}
Database table Question:
Exam_Result can accept your model as a parameter, allowing it to accept anything that your model defines. When you call into your exam function, and you give it your model, you're giving it all of the properties that it needs at once.
public ActionResult Exam_Result(YourModelClass model)
In your model, you'll just have a property that contains a list of the property that you need N of.
public class YourModelClass
{
public int ExamId {get; set;}
public List<Type> ListName { get; set; }
}
You can access the list as it is a property of the model in your view:
#foreach(var listItem in model.ListName)
{
#listItem
}
To send this information back to the view, you can just return it explicity:
return View("ViewName", model);
Since you're using Razor, all you need to do to define the model in your view is add this to the top of the page:
#model Your.Namespace.YourModelClass
For your comment about having different names and values:
public class YourCustomType
{
public string Name {get; set;}
public var WhateverElse { get; set; }
}
#foreach(YourCustomType item in model.YourList)
{
//item.Name
//item.WhateverElse
}
You model binding is wrong.
First of all don't use entity objects in View page, instead create a custom View model and map entity objects and bind it to view page.
You form <form action="/User/Exam_Result"> submits a list of Model (question) but your action only accepts one question (question/option parameters) which is wrong. You should change Exam_Result action parameter to accept a list of questions

List is NULL while POST from View

I use the below Action to allow the user to see a preview of Excel import
[HttpGet]
[Authorize]
public ActionResult ImportVerify()
{
string temp_sessionname = "ImportedData_" + User.Identity.Name;
List<ProjectImportModel> view_model = (List<ProjectImportModel>)TempData[temp_sessionname];
return View(view_model);
}
[HttpPost]
[Authorize]
public ActionResult ImportVerify(List<ProjectImportModel> model)
{
return View(model);
}
And on View i am using a table to show the List of imported data from excel and ask user to confirm the action of import
My view is like this
<h2>Import Verify</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
//table with all details and a submit button in the end
<div class="form-group">
<div class="col-md-10" style="text-align:center;">
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
</div>
}
And model is
public class ProjectImportModel
{
public string Page { get; set; }
public string Author { get; set; }
public string Translator { get; set; }
public string Holder { get; set; }
public string Title { get; set; }
public string TrTitle { get; set; }
//and similar 20 more properties of string type
}
But on POST the list is null
Is any way to get the list back at POST event. My intension is just to allow the preview to user
Or do i need to refill List from TempData # post as well?
In order to post a collection back you need to index the properties, if they're readonly you can just use HiddenFor with a model.
If you want the user to edit them, change them to TextBoxFor's instead or the control that you data requires.
#model List<ProjectImportModel>
<h2>Import Verify</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
//table with all details and a submit button in the end
<div class="form-group">
<div class="col-md-10" style="text-align:center;">
#for(var i = 0 ; i < Model.Count; i++)
{
#Html.HiddenFor(m => m[i].Foo)
#Model[i].Foo <br/>
}
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
</div>
}
I just used a dummy property of Foo without seeing your model.
Obviously you would want to display the data too.
Model Binding To A List
I don't see the snippet code that set your TempData. So I intend that you set it in another Action and then redirect to the ImportVerify Action
TempData keep the information for the time of an HTTP Request. This mean only from one page to another. It's mean that after redirect to your ImportVerify Get Action the data is expired. If you want to keep the data you can try the following ways:
Create a hidden List in your page
Use Session instead of TempData
There are some requirements to post a list back to a controller. Specifically, indexes must be 0-based and unbroken (example, you can bind a list with indexes [0], [1], [2] but not one with [0], [1], [3] or [1], [2], [3].
Alternatively, you could write a custom model binder to parse the request body the way you like.
More on this here: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
EDIT:
Here is an example on how to do it:
Given the following model
public class ExampleModel
{
public int Field1 {get; set;}
public string Field2 {get; set;}
}
And the following actions
[HttpGet]
[Authorize]
public ActionResult ImportVerify()
{
List<ExampleModel> model = //populate the list somehow
return View(model);
}
[HttpPost]
[Authorize]
public ActionResult ImportVerify(List<ExampleModel> model)
{
//do something
}
The example view "ImportVerify.cshtml" would be:
#model List<ExampleModel>
#using(Html.BeginForm())
{
for(var i = 0; i < Model.Count; i++)
{
<div>
#Html.HiddenFor(m => m[i].Field1);
#Html.HiddenFor(m => m[i].Field2);
<p>
Value #i : Field1 = #m[i].Field1 , Field2 = #m[i].Field2
</p>
</div>
}
<input type="submit" value="Send"/>
}
Also, I would revise your logic a bit, to avoid the use of TempData (generally bad practice) and to allow strongly typed views.

ModelView data not passed from View to Controller

My problem is that strongly typed data passed from my Controller to my view comes out empty (all it's properties are null).
I would also like to bind selected value in radiobuttons (marked as QUESTION2) to model property "GivenAnwser" but it doesn't seem to work either.
Type passed around is a ViewModel
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
QuestionViewModel question = Manager.GetQuestion();
return View(question);
}
[HttpPost]
public ActionResult Index(QuestionViewModel question,Anwser givenAnwser)
{
//QuestionViewModel is returned but all it's properties are null.
return View(question);
}
}
VIEW
#model Quiz.ViewModels.QuestionViewModel
#{
ViewBag.Title = "Home Page";
}
#Html.Label("Question:")
#if (Model.CorrectAnwser != null)
{
//some code
}
#Html.DisplayFor(model => model.Question.Text)
//I have tried with Hidden fields and without them
#Html.HiddenFor(model => model.Question)
#Html.HiddenFor(model => model.Anwsers)
#using (Html.BeginForm("Index", "Home"))
{
foreach (var anwser in Model.Anwsers)
{
//QUESTION 2
<input type="radio" name="givenAnwser" value="#anwser" />
<label>#anwser.Text</label>
<br />
}
<input type="submit" value="Check!" />
}
QuestionViewModel
public class QuestionViewModel
{
public QuestionViewModel()
{
this.Anwsers = new List<Anwser>();
}
public Question Question { get; set; }
public List<Anwser> Anwsers { get; set; }
public Anwser GivenAnwser { get; set; }
public bool CorrectAnwser { get; set; }
}
EDIT:
ModelState contains an error:
"The parameter conversion from type 'System.String' to type 'Quiz.Models.Anwser' failed because no type converter can convert between these types."
<input type="radio" name="givenAnwser" value="#anwser" />
this line is setting a complex type "#answer" as the value of the radio button.
this might just do a ToString() of the type during rendering.
When you post it back, MVC might be trying to convert this string value back to
Quiz.Models.Anwser
and failing.
you should probably render
<input type="radio" name="givenAnwser" value="#anwser.SomeBooleanValue" />
p.s. also, why not use the Html Extension to render the radio button.
You can't bind a complex type (Question) to a Hidden field. You would need to bind to the separate child properties.
Also, for the answers, don't use a foreach, use a for loop. Like:
#for(var i=0;i<Answers.Count;i++)
{
<input type="radio" name="#Html.NameFor(a=>a.Answers[i].answer.Value)" value="#Model.Answers[i].anwser.Value" />
}
or
#for(var i=0;i<Answers.Count;i++)
{
#Html.RadioButtonFor(a=>a.Answers[i].answer,Model.Answers[i].answer.Value)
}
Although, that may not be correct either because Answers is a collection of a complex type as well, and you didn't share it's definition.
All in all, I don't think you really need (or want) to post back the entire Model back anyhow. Why not just post back the question ID, and the selected Answer?

Obtaining values for Dynamic checkboxes

I'm pretty new at web development. So, sorry if this I'm asking dumb questions. I'm building a list of checkboxes based on available filter criteria pulled from a database. The model i pass to my view has a property
public List<string> ServicesList{ get; set; }
My view code contains a loop to create a checkbox for each item in the list.
#foreach (string svc in Model.ServicesList)
{
if (svc != "")
{
<input type="checkbox" name="SelectedServices" title="#svc" value="#svc" checked="checked" /> #svc
<br />
}
}
My Controller Action looks like this:
public ActionResult ServiceListExplorer(string[] SelectedServices, FormCollection fc)
{
}
This works as far as displaying the list of services as checkboxes and having access inside the controller action to which ones are checked by the user once the httppost occurs. But, I have 2 questions:
How can I pass which items should be checked initially via the Model? This would allow me to save off their filter criteria and pre-select the services. I was thinking maybe I need to use a SelectList instead of List but not really sure how.
How can I make the "checked" attribute dynamic by binding it to a value in the model?
Instead of creating a list of string objects, you should define a class for e.g.
public class ServiceObject
{
public string Name{get;set;}
public bool IsChecked{get;set;}
}
then your code will become-
public List<ServiceObject> ServicesList{ get; set; }
what you have to fill is the IsChecked property to true if your checkbox is to be selected:
then this line will become-
<input type="checkbox" name="SelectedServices" title="#svc" value="#svc" checked=Model.IsChecked />
First of all instead of ServicesList should be list of objects not list of string :
public class Service
{
public string ServiceName {get; set;}
public bool isChecked {get;set;}
}
....
public List<Service> ServicesList{ get; set; }
....
//that controller will collect data posted from view
//(your form will post only checked checkboxes)
[HttpPost]
public JsonResult Update(FormCollection services)
{
foreach (string item in services)
{
System.Diagnostics.Debug.WriteLine(item);
}
return Json(services);
}
not in your view :
#using (Html.BeginForm("Update", "Home", FormMethod.Post)) {
foreach (MvcMusicStore.Models.Service svc in Model)
{
#svc.ServiceName #Html.CheckBox(svc.ServiceName, svc.isChecked);
<br/>
}
<input type="submit" value="submit"/>
}

Model displays properly but posts back null

I'm populated a partial view with a strongly-typed model. In this partial view is a form. When I submit the form it tells me that objects inside of my model are null, even though they are not because the partial view rendered all elements based on that same model.
More specifically, I'm having trouble passing back all of my checkboxes. If you look at my controller you can see that I check to see if CompanyOptions is null, and every time I run the program it prints STUFF IS NULL, meaning that it's null.
Model:
public class Company
{
public string Name { get; set; }
public string DatabaseName { get; set; }
public CompanyOptions CompanyOptions;
}
public class CompanyOptions
{
public CompanyLicenseOptions CompanyLicenseOptions { get; set; }
}
public class CompanyLicenseOptions
{
public List<CompanyLicenseOption> CompanyLicenseOptionsList;
}
View:
#using (Html.BeginForm("Action", FormMethod.Post))
{
for (int i = 0; i < Model.CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList.Count; i++)
{
#Html.CheckBoxFor(model => model.CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[i].IsLicensed, checkboxHtmlAttributes);
<label for="#Model.CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[i].LicenseName">#Model.CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[i].LicenseName</label>
<br/>
}
#Html.HiddenFor(model => model.DatabaseName)
<input id="submit_licenses" type="submit" style="display:none;" />
}
Controller:
[HttpPost]
public void Action(Company model)
{
System.Diagnostics.Debug.WriteLine("STUFF:" + model.DatabaseName);
if(model.CompanyOptions!=null)foreach (var item in model.CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList) System.Diagnostics.Debug.WriteLine("STUFF:" + item);
else System.Diagnostics.Debug.WriteLine("STUFF IS NULL");
}
Generated HTML:
<input class="licenses" data-val="true" disabled="" id="CompanyOptions_CompanyLicenseOptions_CompanyLicenseOptionsList_0__IsLicensed" name="CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[0].IsLicensed" type="checkbox" value="true" /><input name="CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[0].IsLicensed" type="hidden" value="false" />
The Irrelevant JS
$('#save_licenses').click(function () {
swap_licenses(true);
$('#submit_licenses').click();
});
POST:
Request URL:http://localhost:3080/Controller/Action
Request Method:POST
Status Code:200 OK
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[0].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[1].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[2].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[3].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[4].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[5].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[6].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[7].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[8].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[9].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[10].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[11].IsLicensed:false
CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[12].IsLicensed:false
DatabaseName:myDb
<input class="licenses" data-val="true" disabled="" id="CompanyOptions_CompanyLicenseOptions_CompanyLicenseOptionsList_0__IsLicensed" name="CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[0].IsLicensed" type="checkbox" value="true" /><input name="CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList[0].IsLicensed" type="hidden" value="false" />
Here's your problem:
disabled=""
Your checkbox is disabled, so nothing will ever get sent to the server. That's how HTML works. Disabled elements are never sent. So get rid of this attribute.
If you want to prevent the user from modifying the value, and yet the initial value get sent to the server use the readonly attribute, not disabled.
Also another problem I see with your code is with the CompanyLicenseOptionsList collection field. It should be a property with public getter and setter:
public class CompanyLicenseOptions
{
public List<CompanyLicenseOption> CompanyLicenseOptionsList { get; set; }
}
Same stands true for your CompanyOptions field (you have defined it as a field, whereas it should be a property):
public class Company
{
public string Name { get; set; }
public string DatabaseName { get; set; }
public CompanyOptions CompanyOptions { get; set; }
}
UPDATE:
Now that you have fixed the problem with your missing getters and setters, all that's left is make sure that all the models intervening in this object graph have default (parameterless) constructors. That's a requirement if you want they to appear as action argument because otherwise the default model binder wouldn't know how to instantiate them. If for some reason you cannot add a default constructor to all your objects, I would very strongly recommend you revise your object hierarchy and start using view models right away.
You should use a foreach loop instead of a simple for, this way:
#using (Html.BeginForm("Action", FormMethod.Post))
{
foreach (var option in Model.CompanyOptions.CompanyLicenseOptions.CompanyLicenseOptionsList)
{
#Html.CheckBoxFor(o => o.IsLicensed, checkboxHtmlAttributes);
<label for="#option.LicenseName">#option.LicenseName</label>
<br/>
}
#Html.HiddenFor(model => model.DatabaseName)
<input id="submit_licenses" type="submit" style="display:none;" />
}
Since the endpoint of all these checkboxes is a List<T>, you'll need to make sure that it is instantiated before use:
public class CompanyOptions
{
public CompanyLicenseOptions CompanyLicenseOptions { get; set; }
}
public class CompanyLicenseOptions
{
public List<CompanyLicenseOption> CompanyLicenseOptionsList;
public CompanyLicenseOptions()
{
CompanyLicenseOptionsList = new List<CompanyLicenseOption>();
}
}
EDIT: To ensure that readers get proper context for this answer and avoid confusion, I've reproduced the OP code above mine.

Categories