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.
Related
I am new to MVC and trying to understand viewmodels.
I have Staff, Service, BookingSlot, Appointments and the ApplicationUser entities. I have the following viewmodel:
public class AppointmentBookingViewModel
{
[Display (Name ="Select Staff")]
public int StaffId { get; set; }
public IEnumerable<Staff> Staffs { get; set; }
[Display(Name = "Select Service")]
public int ServiceId { get; set; }
public IEnumerable<Service> Services { get; set; }
[Display(Name = "Select Slot")]
public int BookingSlotId { get; set; }
public IEnumerable<BookingSlot> BookingSlots { get; set; }
}
This is the controller:
public class AppointmentBookingController : Controller
{
private readonly SalonContext _context;
private AppointmentBookingViewModel _appointmentBookingViewModel = new AppointmentBookingViewModel();
public AppointmentBookingController(SalonContext context)
{
_context = context;
ConfigureViewModel(_appointmentBookingViewModel);
}
public void ConfigureViewModel(AppointmentBookingViewModel appointmentBookingViewModel)
{
appointmentBookingViewModel.Staffs = _context.Staffs;
appointmentBookingViewModel.Services = _context.Services;
appointmentBookingViewModel.BookingSlots = _context.BookingSlots;
}
// GET: AppointmentBooking
public ActionResult Index()
{
return View(_appointmentBookingViewModel);
}
}
My question is, how can I create a form in the view and post the data to the Appointments table, the following doesn't work.
#model HairStudio.Services.ViewModels.AppointmentBooking.AppointmentBookingViewModel
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col-12">
<form asp-action="Create">
<div class="form-group">
<label asp-for="ServiceId" class="control-label"></label>
<select asp-for="ServiceId" class="form-control"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
You already directed your form to action called "Create" with asp-action attribute, but there is no such action in your controller. Submitting a form sends a HTTP POST request, which needs to be handled by your controller. Therefore, add a Create() method in your AppointmentBookingController:
// POST: Create
public IActionResult Create(AppointmentBookingViewModel appointmentViewModel)
{
if (!ModelState.IsValid)
{
// Server side validation of form has failed.
// Return to the calling view and inform the user about the errors.
return View(appointmentViewModel, "Index");
}
return View(appointmentViewModel, "<NAME_OF_YOUR_CREATED_APPOINTMENT_VIEW>");
}
Consider redirecting after successfully accepting a HTTP POST request according to a design pattern Post/Redirect/Get.
Also, take a look at this part of ASP.NET Core documentation about working with forms. I'm sure you'll find there something of value.
There's nothing magical about a view model. It's just a class. The idea is that the entity class (i.e. the thing you're persisting to the database via Entity Framework) should be concerned only with the needs of the database. A view can and often does have an entirely different set of needs, so you create a class specifically for that: the view model. This is just basic SRP (single-responsibility principle): a single class shouldn't try to do too much.
Then, you simply need a way to bridge the two. In other words, you need to copy values from the entity to the view model and vice versa. That process is called mapping, and can be achieved in a number of different ways. The most common approach is to use a third-party library like AutoMapper. However, you can also just manually map over each value or even use something akin to the factory pattern, where you have another class that holds the knowledge for how to do the mapping and can spit out an entity from a view model and vice versa.
Now, it's not really possible to give you exact guidance because we don't have your entity(ies), but you seem to be wanting to pick a particular Staff, Service and BookingSlot and associate that with the Appointment you're creating. It's not critical, but for efficiency, you should not be carrying around the full set of all these entities on your view model. All you need is an IEnumerable<SelectListItem>, which allows you to use much more efficient queries:
Instead of the Staffs property, for example:
public IEnumerable<SelectListItem> StaffOptions { get; set; }
Then:
model.StaffOptions = await _context.Staffs.AsNoTracking()
.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() })
.ToListAsync();
In your view:
<select asp-for="StaffId" asp-items="#Model.StaffOptions" class="form-control"></select>
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.
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?
I am just attempting to bind a collection from a form post. My collection in the model is a different name than what I'm trying to bind to, which my be the cause of the issue. I had this working, and then it stopped. I cannot find what changed to make it stop.
I thought my question was similar to this one:
ASP.NET MVC5: Want to update several items in Collection with model binding
And I have read this a few times (Scott Hanselman can be counted on to give a clear picture of such things):
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Here's some code:
public class AttestModel
{
public Account Account { get; set; }
public List<AccountSimpleDetail> AccountSimpleDetails { set; get; }
}
public class AccountSimpleDetResponseModel
{
public int AccountSimpleDetailId { get; set; }
public int AnswerId { get; set; }
}
public partial class AccountSimpleDetail :
{
public int AccountSimpleDetailId { get; set; }
public int? SaqSimpleQuestionId { get; set; }
public int? SaqAnswerId { get; set; }
...More fields go on here
}
In the View I have:
#for (var i = 0; i < Model.AccountSimpleDetails.Count(); i++)
{
#Html.HiddenFor(x => x.AccountSimpleDetails[i].AccountSimpleDetailId)
#Html.HiddenFor(x => x.AccountSimpleDetails[i].AnswerId)
}
I have some Javascript to handle clicking on a bunch of buttons in the browser which sets the AnswerId in the hidden fields. This is working fine.
The markup produced looks like this:
<input data-val="true" data-val-number="The field AccountSimpleDetailId must be a number." data-val-required="The AccountSimpleDetailId field is required." id="AccountSimpleDetails_1__AccountSimpleDetailId" name="AccountSimpleDetails[1].AccountSimpleDetailId" type="hidden" value="71" />
<input data-val="true" data-val-number="The field AnswerId must be a number." id="AccountSimpleDetails_1__AnswerId" name="AccountSimpleDetails[1].AnswerId" type="hidden" value="1" />
The method signature looks like this:
public ActionResult Attest(int id, List<AccountSimpleDetResponseModel> AccountSimpleDetails)
What I used previously (which stopped working) is:
public ActionResult Attest(int id, [Bind(Prefix = "AccountSimpleDetails")] List<AccountSimpleDetResponseModel> simpleDetails)
The result of the bind is always null.
I know when I get to the point where I try something, it doesn't work, I try something else... it simply means I am missing some fundamental point. Thank you for your time.
EDIT
Here is the post request (Raw from Watchlist):
AccountSimpleDetails%5b1%5d.AccountSimpleDetailId=165&AccountSimpleDetails%5b1%5d.AnswerId=1&AccountSimpleDetails%5b2%5d.AccountSimpleDetailId=166&AccountSimpleDetails%5b2%5d.AnswerId=1&AccountSimpleDetails%5b3%5d.AccountSimpleDetailId=167&AccountSimpleDetails%5b3%5d.AnswerId=1&AccountSimpleDetails%5b4%5d.AccountSimpleDetailId=168&AccountSimpleDetails%5b4%5d.AnswerId=1&DXScript=1_171%2c1_94%2c1_93%2c17_33%2c17_2%2c1_152%2c1_164%2c1_91%2c1_156%2c1_101%2c17_7%2c1_154%2c1_103%2c1_102%2c17_8%2c1_114%2c1_121%2c1_169%2c1_138%2c1_170%2c1_124%2c17_9%2c1_163%2c1_162%2c1_147%2c17_32%2c1_157%2c1_98%2c1_125%2c1_104%2c1_166%2c1_139%2c17_13%2c1_97%2c1_141%2c1_142%2c17_15%2c1_155%2c1_143%2c1_144%2c17_16%2c17_17%2c1_126%2c17_11%2c1_146%2c1_149%2c17_20%2c1_160%2c17_22%2c1_158%2c1_153%2c1_161%2c17_25%2c1_165%2c17_28%2c17_31%2c1_100%2c5_5%2c5_4%2c4_11%2c4_10%2c4_6%2c4_7%2c4_9%2c17_14%2c4_12%2c1_113%2c1_116%2c4_13%2c4_14%2c1_110%2c1_112%2c1_137%2c17_12%2c1_159%2c7_49%2c7_47%2c7_51%2c17_21%2c1_105%2c1_108%2c1_117%2c17_0%2c1_120%2c1_106%2c17_1%2c1_107%2c17_3%2c1_109%2c1_122%2c17_5%2c1_145%2c1_119%2c17_18%2c17_19%2c1_118%2c17_29%2c1_123%2c10_2%2c10_1%2c10_3%2c10_4%2c17_4%2c9_23%2c9_22%2c9_24%2c17_24%2c9_13%2c9_10%2c9_8%2c17_23%2c9_12%2c9_9%2c9_15%2c9_11%2c1_96%2c8_10%2c8_17%2c8_24%2c8_26%2c8_9%2c8_12%2c8_13%2c8_18%2c17_26%2c8_21%2c8_23%2c8_22%2c8_16%2c8_19%2c8_20%2c8_14%2c8_15%2c8_25%2c8_11%2c6_12%2c17_30%2c16_16%2c16_18%2c16_14%2c16_11%2c16_19%2c16_6%2c16_15%2c16_8%2c16_12%2c16_13%2c16_7%2c17_27%2c16_17&DXCss=http%3a%2f%2ffonts.googleapis.com%2fcss%3ffamily%3dOpen%2bSans%3a300%2c400%2c600%2c700%2c800%257CShadows%2bInto%2bLight%2c%2fContent%2fbootstrap.min.css%2c%2fContent%2fMainSite.css%2c%2fContent%2fSiteAdjustments.css%2c%2fContent%2ffont-awesome.min.css%2c1_12%2c1_14%2c0_863%2c0_859%2c1_10%2c0_695%2c1_5%2c0_697%2c0_703%2c0_706%2c0_823%2c0_813%2c0_861%2c0_709%2c4_2%2c0_711%2c5_1%2c0_794%2c0_776%2c0_778%2c7_1%2c7_0%2c1_1%2c0_671%2c9_18%2c9_19%2c9_21%2c9_20%2c0_888%2c9_17%2c0_890%2c0_790%2c8_2%2c0_792%2c8_0%2c0_810%2c6_2%2c0_812%2c0_796%2c16_2%2c0_798
You binding your controls to properties of AttestModel so just post back the model
public ActionResult Attest(AttestModel model)
and the AccountSimpleDetails property will be correctly bound to the collection. However looking at your edited post request, it appears that the indexers are starting at 1, not 0 (i.e. the first one is AccountSimpleDetails[1].AccountSimpleDetailId=165.... Do you have any javascript that deletes items in the collection? If so it will not bind the collection because indexers are zero based (and must be consecutive) so you will need to add an additional control for the index property.
I have this:
#for (int x = 0; x < Model.AccountSimpleDetails.Count(); x++)
{
<span>#x</span>
}
And it returns:
1,2, 3
That is the reason for the parsing issues as Stephen pointed out. So that's the real issue.
Thanks
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"/>
}