Dynamic checkboxes - Error: Collection is read-only - c#

I'm quite new to MVC4 and my past experience with asp.net has been working with Webforms, so I think I need some careful hand holding. Thank you in advance!
I'm creating a simple web application that is similar to choosing lotto numbers. At the moment the user chooses whichever numbers they want (no limit, starting with "1") and posts it to the next page where I continue to do something with the numbers the user has chosen. The number of numbers - to choose from is dynamic and represented by checkboxes.
The problem is that when the view is submitted, I get the error page "Collection is read-only."
Model - LottoNumbers.cs
bool[] numbers is to store which numbers the user has checked.
namespace Lotto.Models
{
public class LottoNumbers
{
public string name { get; set; }
public bool[] numbers { get; set; }
public LottoNumbers() {
numbers = new bool[49];
}
public LottoNumbers(int limit)
{
numbers = new bool[limit];
}
}
}
in the HomeController.cs ...
I declared LottoNumbers ln, and
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Lotto.Models;
namespace Lotto.Controllers
{
public class HomeController : Controller
{
public LottoNumbers ln;
public ActionResult Index()
{
return View();
}
public ActionResult SetNumbers()
{
ln = new LottoNumbers(30);
// 30 checkboxes will be generated on the view
return View(ln);
}
[HttpPost]
public ViewResult SetNumbers(LottoNumbers l)
{
// get checkbox values
return View();
}
}
}
View - SetNumbers.cshtml
#model Lotto.Models.LottoNumbers
#{
ViewBag.Title = "SetNumbers";
}
<h2>SetNumbers</h2>
#using (Html.BeginForm()) {
<div>
#Html.LabelFor(m => m.name)
#Html.EditorFor(m => m.name)
</div>
<div id="numberlist">
#for (int i = 0; i < Model.numbers.Length; i++)
{
<div class="item-number" style="">
#Html.EditorFor(m => m.numbers[i])
<label class="lbl-number" for="numbers[#i]">#(i+1)</label>
</div>
}
</div>
<div style="clear:both;">
<input type="reset" value="Reset everything" />
<input type="submit" value="Submit" />
</div>
}

Change your array to a list.
public string Name { get; set; }
public List<bool> Numbers { get; set; }
public LottoNumbers()
{
Numbers = new List<bool>();
for (var i = 0; i < 40; i++)
{
Numbers.Add(false);
}
}
public LottoNumbers(int limit)
{
Numbers = new List<bool>();
for (var i = 0; i < limit; i ++)
{
Numbers.Add(false);
}
}
then your view would be
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(m => m.Name)
#Html.EditorFor(m => m.Name)
</div>
<div id="numberlist">
#for (var i = 0; i < Model.Numbers.Count; i++)
{
<div class="item-number" style="">
#Html.CheckBox("Numbers[" + i.ToString() + "]")
<label class="lbl-number" for="numbers[#(i + 1)]">#(i + 1)</label>
</div>
}
</div>
<div style="clear:both;">
<input type="reset" value="Reset everything" />
<input type="submit" value="Submit" />
</div>
}
Null Reference Exception
You are most likely getting the null reference exception after submitting because you are not passing a model back to the view. You set up your page to expect a LottoNumbers model.
[HttpPost]
public ViewResult SetNumbers(LottoNumbers l)
{
// get checkbox values
return View(); //Nothing being passed to the view
}
so after post when your code gets to here
#foreach (var numberBool in Model.Numbers)
it blows up as the Model is null. You can fix this in several ways depending on how you want your app to run. If you want the user to see a blank list of numbers after submit simply do this and add some sort of success message on post back
[HttpPost]
public ViewResult SetNumbers(LottoNumbers l)
{
//process stuff
ln = new LottoNumbers(30);
return View(ln);
}
you can also simply redirect the user back to the index page. Either way the error is caused because you are not passing in a model with numbers back to the view.

If you look at the stack trace of your "Collection is read only" error, you can see that it's happening during Model Binding, which is when it tried to recreate your LottoNumbers object from the data passed back from the form post.
You're getting the error because you're initialising your object with an array size using you're constructor that takes a parameter, but when it posts back the data, it doesn't know what size the array needs to be when it recreates the object.
To solve this, it would be better to use a List instead, and have the size as a property of your LottoNumbers object.
Using a List means you don't have to pre-initialise the array with a size. And having the size as a property means you can embed the size in a hidden input variable, so that it can be passed back to the controller, and it should correctly do the model binding.

Related

Binding checkBoxList on Razor Page with Viewdata

I want to bind a CheckBoxList on Razor page with Viewdata.I have the following code on my Controller Index:
public async Task<IActionResult> Index()
{
var testSections = new List<ReassignmentSectionLookup>();
testSections = await _employeeService.GetTestSections();
ViewData["testSections"] = new SelectList(testSections, "ReassignmentSectionLookupID", "Section");
return View();
}
I have the following on my razor page:
<div class="form-group row">
<div class="col-md-12">
Select Test: <br />
#{
var select = ViewData["testSections"] as SelectList;
if (select != null && select.ToList().Count > 0)
{
foreach (var item in select.ToList())
{
<input type="checkbox" name="selectedItems" value="#item.Value" #(Html.Raw(item.Selected ? "checked=\"checked\"" : "")) /> #item.Text <br />
}
}
}
</div>
When I hover over select, I can see 12 items in the select:
when I hover over select.ToList().Count I get an error saying "system.NullReferenceException". I am not sure what am I doing wrong. I can see the data in the select. below is the screen shot of the error:
If I try doing select.Items.ToList() then I am getting this error:
When using this line:
<input type="checkbox" name="selectedItems" value="#item.Value" #(Html.Raw(item.Selected ? "checked=\"checked\"" : "")) /> #item.Text <br />
I am getting this error:
As discussed in the comment, I would say SelectList is not suitable as it is used to render the items for the select/drop-down list with either tag helper (asp-items) or HtmlHelper (#Html.DropdownList()).
Create a model class for the data set.
public class ReassignmentSectionLookupOption
{
public string Text { get; set; }
public int Value { get; set; }
public bool Selected { get; set; }
}
Instead of returning the data as SelectList in ViewData, return as List<ReassignmentSectionLookupOption> type.
public async Task<IActionResult> Index()
{
var testSections = await _employeeService.GetTestSections();
ViewData["testSections"] = testSections
.Select(x => new ReassignmentSectionLookupOption { Value = x.ReassignmentSectionLookupID, Text = x.Section })
.ToList();
return View();
}
In View, cast the ViewData as List<ReassignmentSectionLookupOption>. And you can use .Any() to check whether the list is not empty.
#{
var testSections = ViewData["testSections"] as List<ReassignmentSectionLookupOption>;
if (testSections != null && testSections.Any())
{
foreach (var item in testSections)
{
<input type="checkbox" name="selectedItems" value="#item.Value" #(Html.Raw(item.Selected ? "checked=\"checked\"" : "")) /> #item.Text <br />
}
}
}

View providing the same value to Controller

I'm trying to write a simple numbers checking program. You enter a correct next digit - it shows you next one, if not - nothing happens. In the Controller I'm trying to: set a value of variable, increment it, pass to specific View (howManyDigits). I know that I can make howManyDigits a session variable but the point here is to understand why the number is still going back to eg. "2" which was entered in View1 just after the first run of the app.
PiController:
namespace PI.Controllers
{
public class PiController : Controller
{
[HttpGet]
public ActionResult View1()
{
return View();
}
[HttpPost]
public ActionResult View2(Pinumber number)
{
if (number.tabPi[number.howManyDigits] != number.numberEntered)
number.howManyDigits++;
return View(number);
}
}
}
View1:
#model PI.Models.Pinumber
#using (Html.BeginForm("View2", "Pi", FormMethod.Post))
{
#Html.TextBoxFor(number => number.howManyDigits,new { autofocus = "autofocus"})
<input type="submit" value="How many numbers do you know?" />
}
In this view I have BeginForm and i'm trying to pass this variable using HiddenFor and Lambda expression back to the same Controller.
View2:
#model PI.Models.Pinumber
<div>
3.#for (int i = 0; i < Model.howManyDigits; i++)
{
#Model.tabPi[i]
}
</div>
#using (Html.BeginForm("View2", "Pi", FormMethod.Post))
{
#Html.HiddenFor(x=> x.howManyDigits)
#Html.TextBoxFor(x => x.numberEntered, new { autofocus = "autofocus" })
}
Model class Pinumber:
namespace PI.Models
{
public class Pinumber
{
public char[] tabPi { get; set; } = new char[100000];
public Pinumber()
{
for (int i = 0; i < 20; i++)
{
tabPi[i] = piNumber[i];
}
}
public int howManyDigits { get; set; }
public char numberEntered { get; set; }
public string piNumber = "141592653589793238462"
Unfortunately it's going back to Controller with an old value which i entered in other View1 which is used just to display the start site with entry number of digits that you already know - [HttpGet].
Things are not working that way.
Look, if you increment an value of some int variable, from 1 to 2 let's say, and you pass it to view. Then you want to increment it from 2 to 3, you need to send that 2 from View(from HTML code) back to the backend, and increment it there, otherwise, value is lost.

MVC 4 Bind multiple model asp razor

I develop an application who manages formations of employees, I use MVC4 Asp.net with Razor.
In my model I have tow class (who are table in my database) formation and formateur (trainers).
In my application i can create a “formation” and I want to add a list of “formative”(trainers) but I don’t know what I must do.
I think the best solution it’s a list of checkbox, I succeeded to display my list of checkbox with a foreach but I have no idea how I get the result of selected checkbox to pass into my controller.
I saw many tutorials where use “CheckBoxList” and I tried to use too, but I use a ViewBag to populate it and they don't explain how to use it with a viewbag.
Now I test a Dual listBox with tow buttons (Add and Remove) but this doesn't work.
So, somebody can help me to find, and explain how I must do, the good or the best solution ?
I'm sorry for my english, I'm a french girl.
One of my solutions look like this :
My controller :
public ActionResult Create()
{
ViewBag.formateurListe = (from unFormateur in db.salarie
where unFormateur.sFormateur == true
select unFormateur).AsEnumerable()
.Select(m => new SelectListItem
{
Text = m.sNom.ToString() + " " + m.sPrenom.ToString(),
Value = m.sId.ToString()
}).ToList();
return View();
}
[HttpPost]
public ActionResult Create(formation formation, IEnumerable<SelectList> formateurList)
{
if (ModelState.IsValid)
{
db.formation.Add(formation);
foreach (var unSal in formateurList)
{
formateur f = new formateur();
f.ftIdFormation = formation.fId;
f.ftIdSalarie = (int)unSal.SelectedValue;
db.formateur.Add(f);
}
db.SaveChanges();
return RedirectToAction("Index");
}
return View(formation);
}
In my view :
#model MvcAppGestionRH.Models.formation
#using (Html.BeginForm("Create", "Formation", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.ValidationSummary(true)
#Html.Label("Libelle")
#Html.EditorFor(model => model.fLibelle)
#Html.ValidationMessageFor(model => model.fLibelle)
<label id="fFormateur">Formateur</label>
#Html.ListBox("formateurListe", ViewData["formateurListe"] as SelectListItem[], new {Multiple = "multiple"})
<input type="button" value="+" name="add" />
<select name="select" size="7" >
</select>
<input type="submit" value="Créer" />
}
With a script :
$(function () {
$("#Add").click(function () {
$("select").add($('fFormateurListe').selected);
});
});
Checkboxes can be tricky the first time - I googled that a long time, too.
My solution is a view model which looks like this:
It is intended for questions, where the crator can speciy items via checkboxes (e.g. a questions might have the answer "GOOD" and "BAD".
public class QuestionModel
{
public int QuestionID { get; set; }
public string QuestionText { get; set; }
/// <summary>
/// Gets or sets the selected items. Purely a helper List to display check boxes for the user
/// </summary>
/// <value>
/// The selected items.
/// </value>
[Display(Name = "Items", ResourceType = typeof(Domain.Resources.Question))]
public IEnumerable<SelectListItem> SelectedItems { get; set; }
/// <summary>
/// Gets or sets the selected ids. Populated by the user, when he checks / unchecks items. Later translated into QuestionItems
/// </summary>
/// <value>
/// The selected ids.
/// </value>
public int[] SelectedIds { get; set; }
}
This is populated like this in the QuestionController:
private async Task GetSelectedItems(QuestionModel sm, Item selectedItems)
{
var alreadySelected = new List<Scale>();
if (selectedScale != null)
{
alreadySelected.Add(selectedScale);
}
var itemList = (await this.uoW.ItemRepository.Get()).OrderBy(i => i.Name);
sm.SelectedItems = itemList.Select(x => new SelectListItem
{
Value = x.ScaleID.ToString(),
Text = x.NameOfScale.GetText(),
Selected = (from a in alreadySelected where a.ItemID == x.ItemID select x).Any()
});
}
What does this do? It gets a list of all avialable items in the database and populates the model with it. Furthermore, you can pass in a list of items, which are already selected - so you can edit an existing question and siplay all already checked Items.
And n the view I have used a DropDownList:
<div class="form-group">
#Html.LabelFor(model => model.SelectedItems, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<div class="checkbox">
#Html.DropDownListFor(x => x.SelectedIds, Model.SelectedItems, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.SelectedItems, "", new { #class = "text-danger" })
</div>
</div>
</div>
If you want checkboxes, that looks like this(different controller, so dont be confused)
for (int i = 0; i < Model.SelectedItems.Count(); i++)
{
var currentElem = Model.SelectedItems[i];
//if this item is selected by the user, e.g. because he is editing the item, the item will be pre-selected
var selected = currentElem.Selected ? "checked=\"selected\"" : string.Empty;
// column for the questions. expected layout: list of all questions
<div class="col-md-6">
<div class="checkbox" id="SelectedIds">
<label>
<input type="checkbox" value="#currentElem.Value" #selected name="SelectedIds">
#Html.Encode(currentElem.Text)
</label>
</div>
</div>
}
and finally the create() method itself:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "QuestionText,SelectedItems, SelectedIds")] QuestionModel question)
{
if (ModelState.IsValid)
{
// I need only one Item, but if you want ore more change this line
if (question.SelectedIds.Count() == 1)
{
// better use Automapper here, this is unnecessary work
var newQuestion = new Question { QuestionText = question.QuestionText};
var ItemID = question.SelectedIds.First();
newQuestion.QuestionScale = await this.uoW.ItemRepository.GetRaw().Where(i => i.ItemID == ItemD).FirstAsync();
this.uoW.QuestionRepository.Insert(newQuestion);
await this.uoW.Save();
return this.RedirectToAction("Index");
}
else
{
this.logger.Warn("User {0} tried to insert more than one Itemin question {1}", User.Identity.Name, question.QuestionID);
ModelState.AddModelError(string.Empty, xyz.Areas.QuestionManagement.Resources.QuestionRes.ErrorTooManyScales);
}
}
else
{
// the SelectedItems are empty in the model - so if you have to redisplay it, repopulate it.
await this.GetSelectedItems(question, null);
}
return this.View(question);
}
Have you tried using a viewmodel to pass your two model in the view?
For example :
ViewModel
public class CreateFormationViewModel
{
public Formation formation{ get; set; }
public List<Formative> trainers {get;set;}
}
and then use this viewmodel in your view
An easy way to use this view model :
In your controller
public ActionResult CreateFormation()
{
//Get your data (formation and trainer)
CreateFormationViewModel createFormationVM = new CreateFormationViewModel();
createFormationVM.formation = YourFormationModel;
createFormationVM.trainers = YourtrainersModelasList;
//bind data to the view
return View(createFormationVM);
}
And in your View, you have :
#model [yournamespace].CreateFormationViewModel

ASP.NET MVC #Html.EditorFor(model => model.property) But

What about if I have a navigation property? I have a collection of another model in the model I'm trying to edit. I want to display a list of checkboxes for each object in the collection property. So far, this is what I tried...
#{
foreach (var category in ViewBag.Categories)
{
if (Model.Categories.Contains(category))
{
<input type="checkbox" name="selected-categories" value="category.CategoryId" checked="checked" />#category.Name
}
else
{
<input type="checkbox" name="selected-categories" value="#category.CategoryId" />#category.Name
}
}
}
But it fails with an EntityCommandExecutionException. In my if statement, how can I access the model.Categories like I do in something like #Html.EditorFor(model => model.Id)???
Using a strongly-typed View is the way I'd go. Create a ViewModel that contains your Model and use this ViewModel for the strongly-typed View.
Domain Model and ViewModel (simplified)
public class YourModel
{
string Category { get ; set ; }
}
public class YourViewModel
{
public List<string> PossibleCategories { get ; set ; }
public YourModel YourData { get ; set ; }
}
Then the View:
#model YourViewModel
#{
foreach (string CurrCategory in Model.PossibleCategories)
{
if (Model.YourData.Category == CurrCategory)
{
#Html.CheckBox(CurrCategory, new { #checked = "checked" })
#Html.Encode(CurrCategory) <br />
}
else
{
#Html.CheckBox(CurrCategory, false)
#Html.Encode(CurrCategory) <br />
}
}
}

MVC3 Checkbox value won't clear

I have a fairly straightforward razor view which needs to be used for a questionnaire, which is used repeatedly until it runs out questions. Checkbox is used for select answers, the problem is when I first submit the form when the next set of questions returned the answers I submit in the first time are retained in the second page. For the simplicity I have reduced the program as follows,
Model,
public class CheckBoxItemDto
{
public int Id { get; set; }
public bool Selected { get; set; }
}
public class CheckBoxModel
{
public CheckBoxModel()
{
Dtos = new List<CheckBoxItemDto>();
}
public IList<CheckBoxItemDto> Dtos { get; set; }
}
Controller,
public class CheckBoxController : Controller
{
public ViewResult Index()
{
CheckBoxModel model = new CheckBoxModel();
for (int i = 0; i < 5; i++)
{
model.Dtos.Add(new CheckBoxItemDto(){Id = i,Selected = true});
}
return View(model);
}
[HttpPost]
public ViewResult Index(CheckBoxModel mdl)
{
CheckBoxModel model = new CheckBoxModel();
for (int i = 5; i < 10; i++)
{
model.Dtos.Add(new CheckBoxItemDto() { Id = i, Selected = i % 2 == 0 });
}
return View(model);
}
}
View,
#model CheckBoxTest.Models.CheckBoxModel
#{
ViewBag.Title = "ViewPage1";
}
<h2>ViewPage1</h2>
#using (Html.BeginForm())
{
for (int i = 0; i < Model.Dtos.Count; i++)
{
#Html.DisplayFor(m => m.Dtos[i].Id)
#Html.DisplayFor(m => m.Dtos[i].Selected)
#Html.CheckBoxFor(m => m.Dtos[i].Selected)
<br/>
}
<input type="submit" value="Index" />
}
In the view after post, DisplayFor and CheckBoxFor column values are different. But It should be the same.
By the sound of it your ModelState is still holding its previous values when you return the view. Try clearing the ModelState in your post action:
ModelState.Clear();
CheckBoxModel model = new CheckBoxModel();
...
In your Index action method that gets run for a POST, ASP.Net MVC is expecting the resulting View to display validation errors. Therefore, the HTML Helper method CheckBoxFor looks in the ModelState before the Model, so it can display the "invalid" value to the user.
If you're not using the View from a POST action to display errors, you can clear the ModelState as Dangerous suggests. Or, you could use the "Post-Redirect-Get" pattern: in your POST action method, display the same View if there are errors, or else redirect to a GET view that displays something else.

Categories