This question is similar to other questions asked before, but my usage (and MVC knowledge) is different enough so I can't figure out how to adapt the other answers to fit my needs.
I have a form where a user requests a price for a product. That product has a number of optional modules, which affect the overall price. The controller should then send me an email with the chosen module(s)' DisplayName
included.
I can render out these module titles okay in the request form, but cannot read them back when the form is submitted. Debugging shows !ModelState.IsValid
, and within the model state there is a conversion exception:
The parameter conversion from type 'System.String' to type 'MyNamspace.Models.MyProductModule' failed because no type converter can convert between these types.
My entire approach may be wrong, but I've defined the modules in the product model (simplified example for SO), working from this tutorial:
public class MyProductModule
{
public string ModuleName { get; set; }
public bool Checked { get; set; }
}
public class ProductRequest
{
public ProductRequest()
{
Modules = LoadModules();
}
private List<MyProductModule> LoadModules()
{
return new List<MyProductModule>()
{
new MyProductModule() { ModuleName = "Module One", Checked = false },
new MyProductModule() { ModuleName = "Module Two", Checked = false },
new MyProductModule() { ModuleName = "Module Three", Checked = false }
};
}
[DisplayName("MyProduct Modules")]
public List<MyProductModule> Modules { get; set; }
}
Here's the code I use to render the check box list:
@model MyNamespace.Models.ProductRequest
@foreach (var item in Model.Modules)
{
<label>
<input type="checkbox" name="Modules" value="@item.ModuleName" checked="@item.Checked" />
@item.ModuleName
</label>
}
Here's how I am trying to collect the posted form data:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ProcessRequest(ProductRequest qd)
{
if (!ModelState.IsValid)
{
return View("Index", qd); // Code exits here with ModelState error
}
else
{
StringBuilder sb = new StringBuilder();
// null checks removed for brevity...
sb.Append("<ol>");
var selectedModules = qd.Modules.Where(x => x.Checked).Select(x => x).ToList();
foreach (MyProductModule sm in selectedModules)
{
sb.AppendFormat("<li>{0}</li>", sm.ModuleName);
}
sb.Append("</ol>");
}
// ....
}
Any help or advice would be much appreciated. As a long-term webforms developer, I am finding the MVC learning curve relentless!
Solution
See posted answer.
With thanks to Stephen Muecke...
As described in the links suggested, it is possible to refer to the data model without using an instance of the data model. Therefore, this is the correct approach to render the checkboxes:
This ensures that all the modules are listed, even if user doesn't actually select them all, and validation fails.
The model binding is then done automatically when the form is posted so the controller code for processing the form did
not need amendingneed amending slightly. The code to get all the available modules should be static. That way all the modules can be "read"/made available conveniently. Then that static list is compared against the user-submitted list (which may only contains some or none of the available modules):