MVC Not Parsing the Correct Value from Model - asp.net-mvc

Can anyone tell me why, if my model clearly shows my values to be "true" or "false" MVC still thinks it says, "true,false." I assume this is because it is confusing the Request with my Model; however, I am explicitly telling it to use the Model.
As you can see in the image above, the model value is "true." However, in the image below, it thinks the value is "true,false." How can I make it just use "true?"
Model
public class TagCategorySearchViewModel
{
public long Id { get; set; }
public string Name { get; set; }
public List<TagSearchViewModel> Tags { get; set; }
}
public class TagSearchViewModel
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
Controller
[Authorize]
public ActionResult FilterPrereqGrid(EditStudentPrerequisitesViewModel model, int page = 1)
{
model.Prerequisites = new List<PrerequisiteListViewModel>();
var businessPartner = _bpManager.GetBusinessPartnerByMapGuid(model.BusinessPartnerMapGuid);
model.Prerequisites.AddRange(_epManager.GetFullPrerequisitesLeftJoinedWithExperience(model.ExperienceId, businessPartner?.Id));
// fix for how MVC binds checkboxes... it send "true,false" instead of just true, so we need to just get the true
for (int i = 0; i < model.TagCategories?.Count(); i++)
{
for (int j = 0; j < model.TagCategories[i].Tags?.Count(); j++)
{
model.TagCategories[i].Tags[j].IsSelected = bool.Parse((Request.QueryString[$"TagCategories[{i}].Tags[{j}].IsSelected"] ?? "false").Split(',')[0]);
}
}
var selectedTagIds = model.TagCategories?.SelectMany(x => x.Tags).Where(x => x.IsSelected == true).Select(x => x.Id).ToArray();
// filter by selected tags
if (selectedTagIds.Any())
{
model.Prerequisites = (from p in model.Prerequisites
let prereqTagIds = p.Prerequisite.PrerequisiteTags.Select(et => et.TagId)
where selectedTagIds.All(x => prereqTagIds.Contains(x))
select p).ToList();
}
model.Prerequisites = (from m in model.Prerequisites
let ownerDocs = _deManager.GetDocumentsByOwnerAndSourceId(model.BusinessPartnerMapGuid, m.Prerequisite.Id).OrderByDescending(e => e.CreatedDate)
select new PrerequisiteListViewModel
{
Prerequisite = m.Prerequisite,
Selected = m.Selected,
Mandatory = m.Mandatory,
HasExpiration = m.Prerequisite.HasExpiration,
BusinessExpirationPeriod = m.Prerequisite.ExpirationPeriod == 0 ? "None" : m.Prerequisite.ExpirationPeriod.ToString(),
DocOwnerGuid = (ownerDocs.Any() ? model.BusinessPartnerMapGuid : Guid.Empty),
DocRowGuid = (ownerDocs.Any() ? ownerDocs.First().DocRowguid : Guid.Empty),
HasDocuments = ownerDocs.Any()
}).ToList();
int rowsPerPage = 1;
model.TotalRecords = model.Prerequisites.Count();
model.Prerequisites = model.Prerequisites.Skip(page * rowsPerPage - rowsPerPage).Take(rowsPerPage).ToList();
return PartialView("~/Views/BusinessExperience/_EditPrerequisites.cshtml", model);
}

That is by design. The Html.CheckBoxFor extension actually renders something like the following:
<input type="checkbox" .. />
<input type="hidden" />
So both of these have the same name as the property you are rendering out. When checked, the checkbox returns "True", but when unchecked, the checkbox returns nothing. That is the way checkboxes work with form posts. In order to determine that nothing was checked, the MVC framework included a hidden field so that it would return "False" when not checked, but "True,False" when checked (since multiple items with the same name return this way from the form post). And then MVC model binding converts "True,False" to true.
You could just render your own <input type="checkbox" /> with a value of true, and that would just return true, but when unchecked, nothing gets rendered. Be aware of that...

This actually works, it's a shame I can't just do the obvious way and have to write this all out though!
#model List<Prep2PracticeWeb.Models.ViewModels.TagCategorySearchViewModel>
#if (Model != null)
{
<div class="tag-categories">
#for (int i = 0; i < Model.Count(); i++)
{
#Html.HiddenFor(x => Model[i].Id)
#Html.HiddenFor(x => Model[i].Name)
<h4 data-toggle="collapse" data-target="#CollapsableTagCategory_#Model[i].Id" aria-expanded="false" aria-controls="CollapsableTagCategory_#Model[i].Id">
<span class="glyphicon glyphicon-chevron-right"></span>
<span class="glyphicon glyphicon-chevron-down"></span>
#Model[i].Name
</h4>
<div id="CollapsableTagCategory_#Model[i].Id" class="tag-container collapse">
#if (Model[i].Tags != null)
{
for (int j = 0; j < Model[i].Tags.Count(); j++)
{
#Html.HiddenFor(x => Model[i].Tags[j].Id)
#Html.HiddenFor(x => Model[i].Tags[j].Name)
#* the following commented out line won't work because MVC is funny *#
#*<label>#Html.CheckBoxFor(x => Model[i].Tags[j].IsSelected) #Model[i].Tags[j].Name</label>*#
<label>
<input #(Model[i].Tags[j].IsSelected ? #"checked=""checked""" : string.Empty) data-val="true" data-val-required="The IsSelected field is required." id="TagCategories_#(i)__Tags_#(j)__IsSelected" name="TagCategories[#i].Tags[#j].IsSelected" type="checkbox" value="true"> #Model[i].Tags[j].Name
<input name="TagCategories[#i].Tags[#j].IsSelected" type="hidden" value="false">
</label>
}
}
</div>
}
</div>
}
Update: I realized GET was not an appropriate strategy in this scenario given the fact that the querystring can only be so long before the server returns a 500 error. I switched to POST which alleviated the issue.

Related

Passing object from View to Controller ASP.NET MVC

so my intention is to make a button that allow a user to add an item to his Cart, in the db.savechanges method it catch these exceptions :
Property: Item_Name Error: The Item_Name field is required.
iisexpress.exe Information: 0 : Property: Item_Description Error:
The Item_Description field is required. s
i checked from my db and every single item has all their properties filled up, since i am new to asp.net i am surely missing something obvious that is messing with my data.
this is the view(trying to pass the item object in actionlink
#foreach (var item in Model)
{
if (index > 6)
{
index = 0;
}
var newrow = 0;
if (index == 0)
{
newrow = 1;
}
if (newrow == 1)
{
index++;
<div class="product-one">
<div class="col-md-2 product-left">
<div class="p-one simpleCart_shelfItem jwe">
<a href="single.html">
<!-- go to product single view page-->
#{ if (item.Image.Image1 != null)
{
string imageBase64 = Convert.ToBase64String(item.Image.Image1);
string imageSrc = string.Format("data:image/png;base64,{0}", imageBase64);
<img src="#imageSrc" width="100" height="100" />
}
}
#Html.ActionLink("Add to Cart", "AddToCart", "Carts", new { itemdata = item }, new { #class = "btn btn-primary btn-large" })
</a>
<br />
</div>
</div>
</div>
this is the controller:
public ActionResult AddToCart(Item item)
{
//var query = from itemsel in db.Items where item.Item_ID == item.Item_ID select item;
var newcart = new Cart();
newcart.Account_ID = 1;
newcart.Cart_ID = 3;
newcart.Item_ID = item.Item_ID;
newcart.Item = item;
var itemsgroup = db.Items;
try
{
db.Carts.Add(newcart);
db.SaveChanges();
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
Trace.TraceInformation("Property: {0} Error: {1}",
validationError.PropertyName,
validationError.ErrorMessage);
}
}
}
return View(itemsgroup.ToList());
}
and this is the cart model:
public partial class Cart
{
public int Cart_ID { get; set; }
public int Account_ID { get; set; }
public int Item_ID { get; set; }
public virtual Item Item { get; set; }
}
}
p.s i know my view is tricky but i am desperate to finish this project very soon, any suggestion for a better way of doing it would be welcomed
Since you are missing too many pieces, I could only point you to right direction.
Download the sample source code of Pro ASP.NET MVC 5 book By Adam Freeman.
Extract Chapter 9 - SportsStore, and look at those two files-
SportsStore.WebUI > Views > Product > List.cshtml
#foreach (var p in Model.Products) {
#Html.Partial("ProductSummary", p)
}
SportsStore.WebUI > Views > Shared > ProductSummary.cshtml
Notice that it uses BeginForm to add individual item to shopping cart.
#using (Html.BeginForm("AddToCart", "Cart")) {
<div class="pull-right">
#Html.HiddenFor(x => x.ProductID)
#Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<input type="submit" class="btn btn-success" value="Add to cart" />
</div>
}

Action getting no values after submiting checked rows [duplicate]

My viewmodel contains a list of strings:
public class SupplierViewModel
{
public Supplier Supplier { get; set; }
public List<string> Numbers;
}
The user can add any number of strings in the view:
<div class="editor-label">
#Html.LabelFor(model => model.Numbers)
</div>
<div id="allNumbers">
#for (int i = 0; i < Model.Numbers.Count; i++)
{
<div class="editor-label">
#Html.TextBoxFor(m => m.Numbers[i])
</div>
}
</div>
<div id="newNumber" style="display:none;">
<input type="text" name="Numbers[#]" style="display:block;" />
</div>
<button type="button" id="addNumber" >Add</button>
This is done on the client side with jquery:
var container = $('#allNumbers');
$('#addNumber').click(function () {
var index = container.children().length;
var clone = $('#newNumber').clone();
clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
container.append(clone.html());
});
My problem is that none of the dynamically added strings are sent back to the controller even tough they are all named "Numbers[i]".
What am I missing here?
Numbers is a field and the DefaultModelBinder cannot set the value of a field. You need to make it a property by adding getters/setters
public List<string> Numbers { get; set; }

Trying to Post a Model back to the Controller, but it creates a new model instead

Tpa class is my base model.
public class Tpa
{
public bool selected { get; set; }
public int Id { get; set; }
}
Data class creates a list of Tpa objects.
public class Data
{
public List<Tpa> Tpas { set; get; }
public Data()
{
this.Tpas = new List<Tpa>();
this.Tpas.Add(new Tpa()
{
selected = false ,
Id = 1,
});
this.Tpas.Add(new Tpa()
{
selected = false,
Id = 2,
});
this.Tpas.Add(new Tpa()
{
selected = true,
Id = 3,
});
}
}
This is my Get.
[HttpGet]
public virtual ActionResult Index()
{
var model = new Data();
return View(model);
}
This is my view.
#model TpaUpload_2.Models.Data
#using (Html.BeginForm(MVC.TpaUpload_2.Home.ReceiveID(), FormMethod.Post))
<table class="table">
<tr>
#for (int count = 0; count < Model.Tpas.Count; count++)
{
var item = Model.Tpas[count];
<tr>
<td>
<input type="checkbox"
name=#Html.Raw("'s" + count + "CheckBox'")
id=#Html.Raw("'s" + count + "CheckBox'")
#*checked="#(item.selected == true)"*# />
<label for=#Html.Raw("'s" + count + "CheckBox'" )></label>
<input type='hidden'
id=#Html.Raw("'s" + count + "CheckBox'" )
name='item.selected'
value=#Html.Raw("'"+item.selected+"'")/>
</tr>
</table>
<input type="submit" value="Submit" />
This my Post.
[HttpPost]
public virtual ActionResult ReceiveID(Data myData)
{
...
}
I'm trying to use the checkbox value to change the "selected" on the model, and post back the model.
The problem is after the Form is submitted to the Post, the program will construct a new Data object, instead of using the Data model passed to the controller.
What did I do wrong? Any help will be greatly appreciated.
Your constructing html with name attributes that have absolutely no relationship to you model. When you submit, the DefaultModelBinder first initializes your Data model (which means that 3 new Tpa objects are added to its Tpas property. It then tries to find name/value pairs in the form collection that match you model properties but there are none.
First you need to modify you constructor to include only the initialization of the list, and remove the adding of the new items
public class Data
{
public List<Tpa> Tpas { set; get; }
public Data()
{
Tpas = new List<Tpa>();
}
}
And add the items in the GET method
public virtual ActionResult Index()
{
var model = new Data();
model.Tpas .Add(new Tpa(){ selected = false, Id = 1 });
// add other items
return View(model);
}
Then you need to construct you view correctly using the strongly typed html helpers so that your form controls are correctly named in relationship to your model
<table class="table">
#for (int i = 0; i < Model.Tpas.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.Tpas[i].Id)
#Html.CheckBoxFor(m => m.Tpas[i].selected)
#Html.LabelFor(m => m.Tpas[i].selected)
</td>
</tr>
}
</table>
This give your controls the correct name attribute for model binding, for example
<input type="hidden" name="Tpas[0].Id" ... />
<input type="hidden" name="Tpas[1].Id" ... />
<input type="hidden" name="Tpas[2].Id" ... />
I suggest you compare that with what your currently generating to understand the difference.
Note also your current html is invalid - you have multiple <tr> elements inside a <tr> elements and you need to include the hidden input for the Id property or else this will not post back and you will end up with 3 Tpa objects all with id = 0.

ModelState Validation In Multiple Add Scenario

I have a View as Follows
#model List<item>
#using (Html.BeginForm("Create", "Item", FormMethod.Post))
{
#for (int i = 0; i < Model.Count; i++)
{
.....
#Html.EditorFor(model => Model[i].ItemName)
.....
}
<input type="submit" class="btn btn-primary" value="Create Item" />
}
In the ViewModel this ItemName has a Required attribute annotation for Validation purposes, but what I really need is at least one ItemName to be filled to assume that this Model is valid, but I will always get The ModelState IsValid = False
I was able to solve this by using:
public class CreateItemCustomValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
var list = value as List<SingleItem>;
if (list != null)
{
if (list.Where(o => o.ItemName!= null && !String.IsNullOrEmpty(o.ItemName) && !String.IsNullOrWhiteSpace(o.ItemName)).Count() > 0)
{
return true;
}
}
return false;
}
}
The validation works, but the validation is firing before POST

MVC Model binding of complex objects

I've read and implemented many answers here and around the web but had no luck..
My model looks something like this:
public class CampaignModel : BaseModel
{
.
.
public List<TreeItem> Countries { get; set; }
.
.
}
In the view i have:
#foreach (var country in Model.Countries.Select((value,i)=> new {i, value}))
{
<input type="checkbox" name="campaign.Countries[#country.i].Id" value="#country.value.Id" #(country.value.IsSelected ? "checked=\"checked\"" : "") />
}
At the action I have:
[HttpPost]
public ActionResult UpdateTargeting(CampaignModel campaign)
{
return View(campaign);
}
But the 'Countries' property turns out null.
What am I doing wrong?
Thank you
First I think you need to use Model instead of campaign in the name attributes, like below:
name="#Model.Countries[#country.i].Id"
And right now your foreach loop will generate the html code like below:
<input type="checkbox" name="1" value="1"/>
<input type="checkbox" name="2" value="2" checked=""checked""/>
With above code, the model binding will not work, that's why you got null values when you submitted the form. You need something like below:
<input id="Countries_0__IsSelected" name="Countries[0].IsSelected" type="checkbox" value="true"/>
<input name="Countries[0].IsSelected" type="hidden" value="false"/>
<input checked="checked" id="Countries_1__IsSelected" name="Countries[1].IsSelected" type="checkbox" value="true"/>
<input name="Countries[1].IsSelected" type="hidden" value="false"/>
So I suggest you to use Razor syntax, like below:
foreach (var country in Model.Countries.Select((value,i)=> new {i, value}))
{
#Html.CheckBoxFor(m => m.Countries[#country.i].IsSelected )
}
I ended up adding 2 more hidden fields to identify the exact checkbox.
The TreeItem lolks like this:
public class TreeItem
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsSelected { get; set; }
public List<TreeItem> Leafes { get; set; }
public TreeItem() { }
public TreeItem(int id, string title, bool selected = false, List<TreeItem> leafes = null)
{
Id = id;
Title = title;
IsSelected = selected;
Leafes = leafes;
}
}
The full solution looks like this (I demonstrate a a 2 level hierarchy):
<ul id="devicesList">
#foreach (var brand in Model.DeviceBrands.Select((value, i) => new { i, value }))
{
<li>
#Html.CheckBoxFor(m => m.DeviceBrands[#brand.i].IsSelected)
#Html.HiddenFor(m => m.DeviceBrands[#brand.i].Id)
#Html.HiddenFor(m => m.DeviceBrands[#brand.i].Title)
<label><b>#brand.value.Title</b></label>
<ul>
#foreach (var deviceModel in brand.value.Leafes.Select((value, j) => new { j, value }))
{
<li>
#Html.CheckBoxFor(m => m.DeviceBrands[#brand.i].Leafes[#deviceModel.j].IsSelected)
#Html.HiddenFor(m => m.DeviceBrands[#brand.i].Leafes[#deviceModel.j].Id)
#Html.HiddenFor(m => m.DeviceBrands[#brand.i].Leafes[#deviceModel.j].Title)
<label>#deviceModel.value.Title</label>
</li>
}
</ul>
</li>
}
</ul>
Thanks Lin!

Resources