Bind Dictionary with list in viewmodel to checkboxes - c#

How do you properly bind a Dictionary and it's values per key to checkboxes?
I can display them in the HTTPGET but binding the selected values again to HTTPPOST doesn't seem to work.
viewmodel
public class EditViewModel
{
public Foo Foo { get; set; }
public Dictionary<Bar, List<BarVersionEditVM>> Matrix { get; set; }
}
public class BarVersionEditVM
{
public int ID { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public bool IsSupported { get; set; }
}
view:
<form asp-action="Edit">
<div class="row">
#foreach (var kvp in Model.Matrix.OrderByDescending(x => x.Key.Name))
{
<div class="col-md-2 col-lg-2">
<fieldset>
<legend>#kvp.Key.Name</legend>
#foreach (var version in kvp.Value)
{
<div>
<input type="checkbox" id="#version.ID" value="#version.IsSupported" name="#version.Name" #(version.IsSupported ? "checked=\"checked\"" : "") />
<label>#version.Version:</label>
</div>
}
</fieldset>
</div>
}
</div>
<input type="hidden" asp-for="#Model.Foo.ID" />
<input type="submit" value="Save" class="btn btn-default" />
</form>
In the View I tried also to rewrite with foreach and using Html helpers, but without success:
#Html.CheckBoxFor(model => model.Matrix[kvpair.Key][i].IsSupported)
controller:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(EditViewModel vm) {
// vm is there but Matrix are null.
// and only the ID of Foo property is filled in.
}
any suggestions?

Unless your Dictionary has simple value types for both the Key and Value (e.g. public Dictionary<string, string>), the DefaultModelBinder requires that the form control name attributes be in the format
<input .... name="Matrix[0].Key" value="..." />
<input .... name="Matrix[0].Value[0].ID" value="..." />
<input .... name="Matrix[0].Value[0].Name" value="..." />
There are no HtmlHelper methods that will generate the correct html to allow binding to your Dictionary.
It is far simpler to create simple view model(s) to with IList<T> properties for the collections. Based on the view you have shown, those models would be
public class EditVM
{
public int FooID { get; set; }
public List<BarVM> Bars { get; set; }
}
public class BarVM
{
public string Name { get; set; }
public List<BarVersionVM> Versions { get; set; }
}
public class BarVersionVM
{
public int ID { get; set; }
public string Name { get; set; } // not clear where you use this property
public string Version { get; set; }
public bool IsSupported { get; set; }
}
and your view would then be
#model EditVM
....
#Html.HiddenFor(m => m.FooID)
#for(int i = 0; i < Model.Bars.Count; i++)
{
<fieldset>
<legend>#Model.Bars[i].Name</legend>
#Html.HiddenFor(m => m.Bars[i].Name) // in case you need to return the view in the POST method
#for(int j = 0; j < Model.Bars[i].Versions.Count; j++)
{
<div>
#Html.HiddenFor(m => m.Bars[i].Versions[j].ID)
#Html.CheckBoxFor(m => m.Bars[i].Versions[j].IsSupported)
#Html.LabelFor((m => m.Bars[i].Versions[j].IsSupported, Model.Bars[i].Versions[j].Version)
</div>
}
</fieldset>
}
<input type="submit" value="Save" />

Related

How to fix error while trying to pass multiple models to one view in Asp.Net Core 2.1?

I need to pass some models to one view. What I've done so far is something like this:
OrderTypeModel:
namespace test.Models
{
public class OrderTypeModel
{
[Key]
public int Id { get; set; }
public string OrderType { get; set; }
}
}
OrderStatusModel:
namespace test.Models
{
public class OrderStatusModel
{
[Key]
public int Id { get; set; }
public string OrderStatus { get; set; }
}
}
OrderSizeModel:
namespace test.Models
{
public class OrderSizeModel
{
[Key]
public int Id { get; set; }
public string OrderSize { get; set; }
}
}
OrderModel:
namespace test.Models
{
public class OrdersModel
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
public int OrderNumber { get; set; }
public int OrderType_Id { get; set; }
public int OrderStatus_Id { get; set; }
public int OrderSize_Id { get; set; }
public DateTime CreationDate{ get; set; }
}
}
And there's a MultipleViewsModel:
namespace test.Models
{
public class MultipleViewsModel
{
public List<OrderTypeModel> Type { get; set; }
public List<OrderSizeModel> Size { get; set; }
public List<OrderStatusModel> Status { get; set; }
public OrdersModel Orders { get; set; }
}
}
OrderType_Id, OrderStatus_Id, and OrderSize_Id are Foreign Keys in my database. Now When I need to call them all in a form, it throws an error:
My View is something like this:
#model MultipleViewsModel
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Orders.Title" class="control-label"></label>
<input asp-for="Orders.Title" class="form-control" />
<span asp-validation-for="Orders.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Orders.OrderType_Id" class="control-label"></label>
#{
foreach (var ot in ViewData["OrderType"] as Type)
{
<input asp-for="Orders.OrderType_Id" type="radio" value=#ot.Id name=#ot.OrderType. class="form-control" />
}
}
<span asp-validation-for="Orders.OrderType_Id" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
And my Controller is something like this:
public IActionResult Create()
{
ViewData["OrderType"] = _context.OrderType
.Any();
ViewData["OrderSize"] = _context.OrderSize.Any();
return View();
}
I need to put the models in radio forms. But an error is thrown at this line in view:
foreach (var ot in ViewData["OrderType"] as Type)
And says Type is not found in current context.
Note that I've cut a major part of what was not needed and so my form in the view is not complete just to be succinct.
Any kind of help is appreciated.
You can use view model as you have included in your page :
#model MultipleViewsModel
On server side , you should initialize the model class and set the values . For example , you can fill the Type in create action like :
MultipleViewsModel multipleViewsModel = new MultipleViewsModel();
multipleViewsModel.Type = new List<OrderTypeModel>();
multipleViewsModel.Type = db.orderTypes.ToList();
multipleViewsModel.Orders = new OrdersModel() { OrderType_Id = 2 };
return View(multipleViewsModel);
On client side , you can loop the OrderTypeModel list and set default value by which matches the OrderType_Id :
#{
foreach (var orderType in Model.Type)
{
<input asp-for="Orders.OrderType_Id" type="radio" value=#orderType.Id name=Orders.OrderType_Id />
#Html.Label("OrderType" + orderType.Id, orderType.OrderType)
}
}

ASP.NET Core - Bind IEnumerable<T> to a ViewModel field on POST

I have a web application for registering teams to a competition, where each team can select a number of technologies that they will use for their project. The technologies are saved in a Label class.
I am using a view model to bind the information from the form to the action.
However, when I try to submit the form, it takes all other fields, except the list of technologies.
Label.cs
public class Label
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string ColorPalette { get; set; }
}
CreateTeamViewModel.cs
public class CreateTeamViewModel
{
[Required]
public string TeamName { get; set; }
public string ProjectName { get; set; }
public string ProjectDescription { get; set; }
[Required]
public string RepositoryLink { get; set; }
public List<Label> Labels = new List<Label>();
}
TeamsController.cs
public class TeamsController
{
private readonly ApplicationDbContext context;
public IActionResult Create()
{
ViewData["Labels"] = this.context.Labels.ToList();
return View();
}
[HttpPost]
public IActionResult Create(CreateTeamViewModel team)
{
List<Label> labels = team.Labels;
int count = labels.Count; // count = 0
return LocalRedirect("/");
}
}
Create.cshtml (the list of checkboxes)
#model Competition.Data.ViewModels.CreateTeamViewModel
#{
List<Label> labels = ViewData["Labels"] as List<Label>;
}
<form asp-action="Create">
<div class="form-check">
#for(int i = 0; i < labels.Count; i++)
{
<input asp-for="#Model.Labels[i].IsSelected" type="checkbox" />
<label asp-for="#Model.Labels[i].Name">
<span class="badge badge-#labels[i].ColorPalette">#labels[i].Name</span>
</label>
<input asp-for="#Model.Labels[i].Name" type="hidden" value="#labels[i].Name" />
<input asp-for="#Model.Labels[i].ColorPalette" type="hidden" value="#labels[i].ColorPalette" />
}
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
You need to bind to a list of int instead of a list of Label on your view model. Then, you'll need to use that list of selected ids to fill your list of labels on the Team entity you're persisting:
public class CreateTeamViewModel
{
[Required]
public string TeamName { get; set; }
public string ProjectName { get; set; }
public string ProjectDescription { get; set; }
[Required]
public string RepositoryLink { get; set; }
public List<int> SelectedLabels { get; set; } = new List<int>();
}
Then, you'll need to modify your form to bind your checkboxes to this list:
#foreach (var label in labels)
{
<input asp-for="SelectedLabels" id="Label#(label.Id)" value="#label.Id" type="checkbox" />
<label id="Label#(label.Id)">
<span class="badge badge-#label.ColorPalette">#label.Name</span>
</label>
}
Notice that I removed the hidden inputs. You should never post anything that the user should not be able to modify, as even hidden inputs can be tampered with.
After posting, server-side you'll end up with a list of label ids that were selected by the user. Simply query the associated labels out of your database and then assign that to the team you're creating:
team.Labels = await _context.Set<Label>().Where(x => model.SelectedLabels.Contains(x.Id)).ToListAsync();

Controller not able to read model returned from View in MVC

I am using MVC framework in my project and using model binding to fill a view and then retrieve the values from the view using two controllers.
I am able to fill the view with the model properties but when i am trying to retrieve the values on click of a submit button. The model returns null properties.
Here is the ViewModels:
public partial class QuestionAnswer
{
public string QuestionText { get; set; }
public string QuestionId { get; set; }
public string AnswerText { get; set; }
public string SectionName { get; set; }
public bool IsChecked { get; set; }
}
public partial class pluralQuestionAnswer
{
public Dictionary<int, QuestionAnswer> QnAs { get; set; }
public Dictionary<string, int> SectionNameWithRank { get; set; }
public Dictionary<string, int> SectionNameWithQuestionCount { get; set; }
}
Here are the controllers:
namespace Temp.Controllers
{
public partial class AIPController : Controller
{
public ActionResult FirstInitiative()
{
pluralQuestionAnswer ViewModel = new pluralQuestionAnswer();
//code to fill the model ViewModel
return View(ViewModel);
}
[HttpPost]
public ActionResult SaveSelectedInitiatives(pluralQuestionAnswer ViewModel, string Save)
{
//some code that uses ViewModel
//this is where ViewModel shows all properties as null
return View();
}
}
}
FirstInitiative is used to generate the view by passing a viewModel and SaveSelectedInitiatives is used to get value from the viewModel retrieved from the view.
Here is the View:
#using Models;
#model pluralQuestionAnswer
#{
ViewBag.Title = "FirstInitiative";
int sectionIterator;
int sectionPracticeIterator;
int totalPracticeIterator = 1;
}
<header></header>
<section>
<header>
<h1>First Initiative</h1>
</header>
#using (Html.BeginForm("SaveSelectedInitiatives", "AIP", FormMethod.Post))
{
<div class="inititive-table" style="overflow:auto">
<div class="inititive-table-header">
<div class="initiative-questioncolumn">
Common Minimum Practices
</div>
<div class="initiative-answercolumn">
Your response
</div>
</div>
#for (sectionIterator = 1; sectionIterator <= Model.SectionNameWithRank.Count; sectionIterator++)
{
<div class="initiative-section">
<div class="initiative-section-name">
<span>#Model.SectionNameWithRank.Keys.ElementAt(sectionIterator - 1)</span>
</div>
#for (sectionPracticeIterator = 1; sectionPracticeIterator <= Model.SectionNameWithQuestionCount[Model.SectionNameWithRank.Keys.ElementAt(sectionIterator - 1)]; sectionPracticeIterator++)
{
<div class="initiative-section-question initiative-questioncolumn">
<label>
#*model binding*#
#Html.HiddenFor(x => x.QnAs[totalPracticeIterator].QuestionId)
<span style="width: 20px; float:left">#Html.CheckBoxFor(x => x.QnAs[totalPracticeIterator].IsChecked)</span>
<span style="display: block; margin-left: 20px">#Html.Raw(Model.QnAs[totalPracticeIterator].QuestionText)</span>
</label><br />
</div>
<div class="initiative-section-answers initiative-answercolumn">
#Model.QnAs[totalPracticeIterator].AnswerText
</div>
{totalPracticeIterator += 1;}
}
</div>
}
</div>
<div>
<input type="submit" class="zs-left zs-button zs-button-action zs-atp-button" name="Save" value="Save" />
</div>
}
</section>
In your Dictionary objects, change the key from int type to string type and this should work.
You may want to rename your inputs to match the KeyValuePair schema of your model, so replace below:
#Html.HiddenFor(x => x.QnAs[totalPracticeIterator].QuestionId)
With
#Html.Hidden(string.Format("QnAs[{0}].Key", i), /*<You Need Key's value here> Like: (_kvp.Key) */)
#Html.Hidden(string.Format("QnAs[{0}].Value.QuestionId", i), x.QnAs[/*<You Need Key's value here> Like: (_kvp.Key) */].QuestionId)
You may try, below code to get KeyValuePair:
var _kvp = Model.QnAs.ElementAt(totalPracticeIterator)
So, the posted data from browser would look like:
QnAs[0].Key = 1
QnAs[0].Value.QuestionId = 123
QnAs[1].Key = 32
QnAs[1].Value.QuestionId = 55

MVC 4 Model Binding returns null

I'm having trouble with model binding in MVC. I have a class:
public class UserSurvey
{
public int Id { get; set; }
public virtual Survey Survey { get; set; }
}
Which is the model for a view:
#model SurveyR.Model.UserSurvey
<form id="surveyForm">
<div class="container survey">
#Html.HiddenFor(x=>x.Id)
#Html.EditorFor(x => x.Survey.Steps)
</div>
<input type="button" value="Submit" id="btnSubmit"/>
</form>
And then for the submit the controller takes a class:
public class SurveyResponseViewModel
{
public int Id { get; set; }
public Survey Survey { get; set; }
}
[HttpPost]
public ActionResult Submit(SurveyResponseViewModel surveyResponse)
{
...
}
When I debug the submit the surveyResponse.Survey object is populated as it should be but the surveyResponse.Id value is 0 when it should be 1.
I can see the Id=1 being passed back in the submit but the model binding doesn't seem to hook it up.
Any help would be greatly appreciated!
Kev
EDIT: The rendered html looks like this:
<form id="surveyForm">
<div class="container survey">
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Id" name="Id" type="hidden" value="1" />
So yes the value appears there and is also passed in the submit if I look using dev tools.
EDIT 2: The Form data in dev tools definitely contains "Id:1".
Your Code seems to be fine.Try passing the id value explicitly as another parameter like below
[HttpPost]
public ActionResult Submit(SurveyResponseViewModel surveyResponse , int Id )
{
surveyResponse.Id = Id
}
I have tested. Its working fine.
public ActionResult test1()
{
var model = new UserSurvey();
model.Id = 10;
return View(model);
}
[HttpPost]
public ActionResult test1(SurveyResponseViewModel surveyResponse)
{
var x = surveyResponse.Id; // returns 10
return View(new UserSurvey());
}
public class SurveyResponseViewModel
{
public int Id { get; set; }
public Survey Survey { get; set; }
}
public class UserSurvey
{
public int Id { get; set; }
public virtual Survey Survey { get; set; }
}
public class Survey
{
public string Steps { get; set; }
}
#model TestWeb.Controllers.UserSurvey
#using (Html.BeginForm())
{
<div class="container survey">
#Html.HiddenFor(x=>x.Id)
#Html.EditorFor(x => x.Survey.Steps)
</div>
<input type="submit" value="Submit" id="btnSubmit"/>
}

C# MVC - Razor Complex Nested Form Submission not binding

Long time lurker... first time question asker...
I have a complex form which returns null when being submitted. Essentially I am trying to build a database driven forms.
The form contains a list of either sections or questions
A section contains a list of either another section, or questions
Model 1:
public FormViewModel {
public List<FormSetsViewModel> formSets { get; set; }
}
Model 2:
public FormSetsViewModel{
QAViewModel questionAnswerViewModel { get; set; }
SectionViewModel sectionViewModel { get; set; }
bool isQuestion { get; set; }
bool isSection { get; set; }
}
Model 3:
public SectionViewModel {
public List<FormSectionQuestionsViewModel> formSectionQuestions { get; set; }
}
Model 4:
public FormSectionQuestionsViewModel {
public QuestionAnswerViewModel questionAnswers;
public SectionViewModel childSection;
int orderNumber;
}
Model 5:
public QAViewModel {
int id { get; set; }
string answer { get; set; }
string question { get; set;}
}
The views are as follows:
FormViewModel.cshtml
#model FormViewModel
#using (Html.BeginForm("Save", "Forms"))
{
<div class="row">
#Html.EditorFor(model => model.formSetsViewModels)
</div>
<div class="controls">
<input type="submit" value="Confirm" class="button" name="save" />
</div>
}
#model FormSetsViewModel
<div class="control-group">
#if (Model.isQuestion)
{
#Html.EditorFor(m => m.questionViewModel);
}
else
{
#Html.EditorFor(m => m.sectionViewModel);
}
</div>
SectionViewModel.cshtml
#model SectionViewModel
#Html.EditorFor(m => m.formSectionQuestions)
FormSectionQuestionsViewModel.cshtml
#model FormSectionQuestionsViewModel
#if (Model.childSection != null)
{
#Html.EditorFor(m => m.childSection)
}
else
{
#Html.EditorFor(m => m.questionAnswers)
}
QAViewModel.cshtml
#model QAViewModel
<p><div class="question-text-edit">#Html.Raw(Model.questionText)</div>
#Html.TextAreaFor(m => m.answer, new { style = "width: 90%; height: 80px;" })
The controller:
[HttpPost]
public ActionResult Save(int caseID, List<FormSetsViewModel> formSets = null)
{
return Index(caseID);
}
The view works great as a database driven form. However, when I submit the form, it seems that the formsets cannot bind, and returns null.
From Html, it created an input like this:
<input id="formSetsViewModels_d762713a-7a2f-497a-9417-4c6e91d33cb8__sectionViewModel_formSectionQuestions_48e738da-10d3-4518-be59-2493e2b7a7cc__questionAnswers_answer" name="formSetsViewModels[d762713a-7a2f-497a-9417-4c6e91d33cb8].sectionViewModel.formSectionQuestions[48e738da-10d3-4518-be59-2493e2b7a7cc].questionAnswers.answer" type="text" value="">
Finally found the answer!
The variable name for the FormSetsViewModel in the
public ActionResult Save(int caseID, List<FormSetsViewModel> formSets = null)
needs to be formSetsViewModel for the model to be able to be binded.
The other thing is that, some public variables in the class does not have { get; set; } method.
All variables that we want to be bind needs the { get; set; } method. Adding this solve the issue.

Categories