MVC - Problem with DataBinding to a Collection using a Custom Template - asp.net-mvc

I am trying to bind to a Model that has a collection property, specifically a List. For the purposes of this example, this represents a list of user roles:
public class RolesModel
{
private List<SelectListItem> _Roles = null;
public string Name { get; set; }
public List<SelectListItem> Roles
{
get {
if (_Roles == null) { _Roles = new List<SelectListItem>(); }
return _Roles;
}
set { _Roles = value; }
}
}
I am binding this to a strongly-typed view via the following Controller:
public class TestController : Controller
{
RolesModel myModel = new RolesModel();
[HttpGet]
public ActionResult Edit()
{
myModel.Name = "Joe Bloggs";
myModel.Roles = new List<SelectListItem>
{
new SelectListItem { Value = "1", Text = "Member", Selected = true },
new SelectListItem { Value = "2", Text = "Manager", Selected = true },
new SelectListItem { Value = "3", Text = "Administrator", Selected = false }
};
return View(myModel);
}
[HttpPost]
public ActionResult Edit(RolesModel m)
{
// !!! m.Roles is always empty !!!
return View("Results", m);
}
}
This then invokes the following view:
#model MyProject.WebUI.Models.RolesModel
#using (Html.BeginForm())
{
<p>
#Html.LabelFor(m => m.Name)
#Html.EditorFor(m => m.Name)
</p>
<div>
#Html.EditorFor(m => m.Roles, "CheckBoxList")
</div>
<p>
<input type="submit" value="Save" />
</p>
}
Note the template specific call to my custom editor template in '/Views/Shared/EditorTemplates/CheckBoxList.cshtml' this looks like this:
#model List<System.Web.Mvc.SelectListItem>
<h3>Type: #Html.LabelFor(m => m)</h3>
<ul>
#for (int i = 0; i < Model.Count; i++)
{
<li>
#Html.CheckBoxFor(m => m[i].Selected)
#Html.LabelFor(m => m[i].Selected, Model[i].Text)
#Html.HiddenFor(m => m[i].Value)
</li>
}
</ul>
The idea being that each SelectListItem is represented by the Html rendered by the loop.
The first part of the process appears to work correctly, The form is presented as expected and you can update the 'Name' text box and the check/uncheck the checkboxes.
The problem is that when the form is posted back to the controller, the Roles collection is never populated.
I'm new to MVC and thought that the framework actually re-constructed the model data from the post via the enforced form element naming convention. I'm obviously missing an important point and I'm hoping someone can point me in the right direction.
Thanks, and apologies for the long post.

Here's how you could proceed:
#model MyProject.WebUI.Models.RolesModel
#using (Html.BeginForm())
{
<p>
#Html.LabelFor(m => m.Name)
#Html.EditorFor(m => m.Name)
</p>
<div>
<ul>
#Html.EditorFor(m => m.Roles)
</ul>
</div>
<p>
<input type="submit" value="Save" />
</p>
}
and inside the EditorTemplate (/Views/Shared/EditorTemplates/SelectListItem.cshtml):
#model System.Web.Mvc.SelectListItem
<h3>Type: #Html.LabelFor(m => m)</h3>
<li>
#Html.CheckBoxFor(m => m.Selected)
#Html.LabelFor(m => m.Selected, Model.Text)
#Html.HiddenFor(m => m.Value)
</li>
Notice the simplification of the editor template. It no longer takes a List<SelectListItem> as model but simply a SelectListItem. It will automatically be invoked for each element of the Roles collection so that you don't need to write any loops. Just follow the conventions.
I would also simplify your view model like this:
public class RolesModel
{
public string Name { get; set; }
public IEnumerable<SelectListItem> Roles { get; set; }
}
and your controller:
public class TestController : Controller
{
public ActionResult Edit()
{
var myModel = new RolesModel
{
Name = "Joe Bloggs",
Roles = new[]
{
new SelectListItem { Value = "1", Text = "Member", Selected = true },
new SelectListItem { Value = "2", Text = "Manager", Selected = true },
new SelectListItem { Value = "3", Text = "Administrator", Selected = false }
}
};
return View(myModel);
}
[HttpPost]
public ActionResult Edit(RolesModel m)
{
// m.Roles should be correctly bound
return View("Results", m);
}
}

Related

Model binding with complex type

I have made a test controller and view to test complex binding, but I can't seem to make it work.
Here is my ViewModel:
public class TestViewModel
{
public SubTest MainTest { get; set; }
public List<SubTest> SubTestList { get; set; }
}
public class SubTest
{
public string Name { get; set; }
public int Id { get; set; }
}
Here is my View:
#model TestViewModel
#{
using (Html.BeginForm())
{
<h2>Main</h2>
<p>
#Html.DisplayTextFor(m => m.MainTest.Id)
=>
#Html.DisplayTextFor(m => m.MainTest.Name)
</p>
<h2>Subs</h2>
foreach (var sub in Model.SubTestList)
{
<p>
#Html.DisplayTextFor(m => sub.Id)
=>
#Html.DisplayTextFor(m => sub.Name)
</p>
}
<button type="submit">Submit</button>
}
}
And here is my controller:
public ActionResult Test()
{
TestViewModel tvm = new TestViewModel();
tvm.MainTest = new SubTest() { Id = 0, Name = "Main Test" };
tvm.SubTestList = new List<SubTest>()
{
new SubTest() { Id = 1, Name = "Sub Test 1" } ,
new SubTest() { Id = 2, Name = "Sub Test 2" } ,
new SubTest() { Id = 3, Name = "Sub Test 3" } ,
new SubTest() { Id = 4, Name = "Sub Test 4" } ,
};
return View(tvm);
}
[HttpPost]
public ActionResult Test(TestViewModel tvm)
{
return View(tvm);
}
When I load the page, everything displays correctly, but if I set a breakpoint in the POST method, I see that the parameter values are both null.
What am I doing wrong ?
Firstly DisplayTextFor() does not generate form controls (input, textarea, select) therefore there is nothing for the form to post back.
Secondly, if you did want to edit the values of your model (say using a textbox), then you would need to use a for loop (or custom EditorTemplate for typeof SubTest) not a foreach loop for your collection property, for example
for (int i = 0; i < Model.SubTestList.Count; i++)
{
#Html.TextBoxFor(m => m.SubTestList[i].Id)
#Html.TextBoxFor(m => m.SubTestList[i].Name)
}
Or using an EditorTemplate (the name of the template must match your model type
In /View/Shared/EditorTemplates/SubTest.cshtml
#model yourAssembly.SubTest
#Html.TextBoxFor(m => m.Id)
#Html.TextBoxFor(m => m.Name)
and in the main view
#model TestViewModel
....
#Html.EditorFor(m => m.SubTestList)
The EditorFor() method accepts IEnumerable<T> and is smart enough to rendered the html from the template for each item in the collection.

PartialView data not posting back to controller

I am loading the partial view based on dropdownlist value as suggested in the following links. I am able to show partial View and enter values in textboxes. But when I postback, I am unable to get values in Controller. I am getting all other values except this partial view values
Render Partial View Using jQuery in ASP.NET MVC
div id="divFloorPlans"></div>
$('#ddlFloorPlans').change(function () {
var numberOfFloorPlans = $(this).val();
var data = { "id": numberOfFloorPlans };
$.ajax({
url: "FloorPlans",
type: "POST",
data: data, //if you need to post Model data, use this
success: function (result) {
$("#divFloorPlans").html("");
$("#divFloorPlans").html(result);
}
});
});
#model IList<ViewModels.FloorPlan>
#for (int i = 1; i <= Model.Count; ++i)
{
<div class="col-md-12" >
<div class="col-md-2" >
#Html.DropDownListFor(m => m[i - 1].Bedrooms, ViewData["Bedrooms"] as List<SelectListItem> })
</div>
<div class="col-md-2" >
#Html.DropDownListFor(m => m[i - 1].Bathrooms, ViewData["Bathrooms"] as List<SelectListItem> })
</div>
<div class="col-md-3" >
#Html.TextBoxFor(m => m[i - 1].MinPrice})
#Html.TextBoxFor(m => m[i - 1].MinPrice })
</div>
<div class="col-md-3">
#Html.TextBoxFor(m => m[i - 1].MinSqFt, new { #placeholder = "From" })
#Html.TextBoxFor(m => m[i - 1].MaxSqFt, new { #placeholder = "To"})
</div>
</div>
}
My Model looks like this.
public class ItemEditVM
{
public FloorPlan FloorPlan { get; set; }
public IList<FloorPlan> ListFloorPlans { get; set; }
}
My Controllers
//Partial View Returning Controller
[HttpPost]
public ActionResult FloorPlans(int id)
{
var model = new ItemEditVM();
model.NumOfFloorplans = id;
model.ListFloorPlans = new List<FloorPlan>();
for (int i = 0; i < id; i++)
{
model.ListFloorPlans.Add(new FloorPlan { FloorPlanName = "", Bathrooms = "", Bedrooms = "", MaxPrice = "", MinPrice = "", MaxSqFt = "", MinSqFt = "" });
}
return View("FloorPlan", model.ListFloorPlans);
}
//Create Controller
[HttpPost]
public ActionResult Create(ItemEditVM model)
{
if (ModelState.IsValid)
{
}
}
You partial views model is IList<FloorPlan> which is generating controls with
<input name="[0].MinPrice" ..>
<input name="[1].MinPrice" ..>
which would post back to IList<FloorPlan> model, but your method parameter is ItemEditVM model. You need to the partial view model to be #model ItemEditVM
In the GET method
return View("FloorPlan", model);
and in the view
#model ItemEditVM
#for (int i = 1; i <= Model.ListFloorPlans.Count; ++i)
{
....
#Html.TextBoxFor(m => m.ListFloorPlans[i - 1].MinPrice})
....
}
which will generate the correct name attributes for binding to your model
<input name="ListFloorPlans[0].MinPrice" ..>
<input name="ListFloorPlans[1].MinPrice" ..>

How to bind List<X> to ViewModel

I have 2 ViewModels - User and Reminder.
public class UserViewModel
{
[Required]
[Display(Name = "Your name")]
public string Name { get; set; }
[Display(Name = "Your reminders")]
public IEnumerable<ReminderViewModel> Reminders { get; set; }
}
public class ReminderViewModel
{
[Required]
[Display(Name = "Time")]
public TimeSpan Time { get; set; }
[Required]
[Display(Name = "Frequency of repair")]
public string Frequency { get; set; }
}
My add-view:
#using (Html.BeginForm("Add", "Test"))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#ViewBag.Status
<ul style="list-style: none;">
<li>
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
#Html.ValidationMessageFor(m => m.Name)
</li>
???????????? // Reminders
<li>
<input type="submit" value="Add" /></li>
</ul>
}
My question is: how can I bind this IEnumerable<ReminderViewModel> in my View?
I want to achieve situation, where user can put some reminders (selecting Time and Frequency) many times, before send click.
How can I do this?
You need to do the following:
First add a reference to the model in the top of the view like this:
#model UserViewModel
Including your namespace.
Change your collection to user a list so it can be indexed and model binded like this:
#for (var i = 0; i < Model.Reminders.Count(); i++)
{
#Html.LabelFor(m => Model.Reminders[i].Frequency)
#Html.TextBoxFor(m => Model.Reminders[i].Frequency)
#Html.ValidationMessageFor(m => Model.Reminders[i].Frequency)
#Html.LabelFor(m => Model.Reminders[i].Time)
#Html.TextBoxFor(m => Model.Reminders[i].Time)
#Html.ValidationMessageFor(m => Model.Reminders[i].Time)
}
This in your model:
[Display(Name = "Your reminders")]
public List<ReminderViewModel> Reminders { get; set; }
You also need to change your Begin form to set it to post like this:
#using (Html.BeginForm("Add", "Test", FormMethod.Post))
Finally you need to implement the post like this taking the model as a parameter:
[HttpPost]
public ActionResult Add(UserViewModel model)
{
// Do processing here
return View(model);
}
First, you need to change it to List<ReminderViewModel>. In order to get proper model binding you must be able to select the objects using an index value.
Then in your view:
#for (var i = 0; i < Model.Reminders.Count(); i++)
{
#Html.LabelFor(m => m.Reminders[i].Time)
#Html.TextBoxFor(m => m.Reminders[i].Time)
#Html.ValidationMessageFor(m => m.Reminders[i].Time)
#Html.LabelFor(m => m.Reminders[i].Frequency)
#Html.TextBoxFor(m => m.Reminders[i].Frequency)
#Html.ValidationMessageFor(m => m.Reminders[i].Frequency)
}
If Reminders is initially null, you'll need to initialize it with one or more ReminderViewModels in your action:
var model = new UserViewModel {
Reminders = new List<ReminderViewModel> {
new ReminderViewModel()
}
}
return View(model);
Or you can do this in your UserViewModel's constructor:
public class UserViewModel
{
public UserViewModel()
{
Reminders = new List<ReminderViewModel> {
new ReminderViewModel();
}
}
}

ASP.NET MVC Generic List of Different SubClasses

I have an MVC model with a property that contains a generic collection of types that inherit from a single type. It displays the editor exactly as I would expect, but when I post back the types of all the items are the base type. How do I get it to return the correct types?
Model...
public class PageEM {
public long Id { get; set; }
public virtual IList<FieldEM> Fields { get; set; }
}
public class FieldEM { // I'd really like this to be abstract.
public long Id { get; set; }
public string Caption { get; set; }
public string Value { get; set; }
}
public class TextFieldEM : FieldEM {
}
public class CheckBoxFieldEM : FieldEM {
public bool ValueData {
get { return (bool)Value; }
set { Value = (string)value; }
}
PageEM View...
#model PageEM
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Fields</legend>
#Html.HiddenFor(m => m.Id)
#Html.EditorFor(m => m.Fields)
<input type="submit" value="Submit" title="Submit" />
</fieldset>
}
TextFieldEM Editor...
#model TextFieldEM
<div>
#Html.HiddenForFor(m => m.Id)
<div>
#Html.LabelFor(m => m.Value, Model.Caption)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.Value)
#Html.ValidationMessageFor(m => m.Value)
</div>
</div>
CheckBoxFieldEM Editor...
#model CheckBoxFieldEM
<div>
#Html.HiddenForFor(m => m.Id)
<div class="editor-field">
#Html.EditorFor(m => m.DataValue)#Html.LabelFor(m => m.DataValue, Model.Caption, new { #class = "checkbox" })
</div>
</div>
Controller...
public partial class PageController : Controller {
public virtual ActionResult Edit() {
PageEM em = new PageEM() {
Id = 123,
Fields = new List<FieldEM>() {
new TextFieldEM() { Id = 1, Caption = "Text Line", Value = "This is test" },
new CheckBoxEM() { Id = 2, Caption = "Check here", ValueData = true }
}
};
return View(em);
}
[HttpPost]
public virtual ActionResult Edit(PageEM em) {
if (!ModelState.IsValid)
return View(em);
// but all of the em.Fields are FieldEM.
}
}
So how do I get it to post back with the subclassed FieldEMs?
You can't do that with the DefaultModelBinder. You'll have to create your own custom model binder in order to do what you want to do.
These might be helpful:
https://gist.github.com/joelpurra/2415633
ASP.NET MVC3 bind to subclass
ASP.NET MVC 3: DefaultModelBinder with inheritance/polymorphism

problems with html helper method

I can't figure out how to send a parameter from a dropdownlist to my model. Could someone please show me an example of how to do this?
As always you start by defining a model:
public class MyViewModel
{
public string SelectedValue { get; set; }
public SelectList Items
{
get
{
return new SelectList(new[]
{
new SelectListItem { Value = "1", Text = "item 1" },
new SelectListItem { Value = "2", Text = "item 2" },
new SelectListItem { Value = "3", Text = "item 3" },
}, "Value", "Text");
}
}
}
Controller:
public class HomeController: Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
// You will get the selected value inside model.SelectedValue here
// => do something with it
....
}
}
Strongly typed view:
<% using (Html.BeginForm()) { %>
<%= Html.DropDownListFor(x => x.SelectedValue, Model.Items) %>
<input type="submit" value="OK" />
<% } %>
public ActionResult Edit(int id)
{
Affiliate affiliate = affiliateRepository.GetAffiliate(id);
List<SelectListItem> StateList = new List<SelectListItem>();
SelectListItem item;
Dictionary<string, string> dictState = S127Global.Misc.getStates();
foreach (KeyValuePair<string, string> k in dictState)
{
item = new SelectListItem();
item.Text = k.Value;
item.Value = k.Key;
StateList.Add(item);
}
item = new SelectListItem();
item.Text = " - Select State - ";
item.Value = "";
StateList.Insert(0, item);
//create new select list with affiliate.state as the selected value in ViewData
ViewData["State"] = new SelectList(StateList.AsEnumerable(), "Value", "Text",affiliate.State);
return View(affiliate);
}
code for view
<div class="editor-label">
<%: Html.LabelFor(model => model.State) %>
</div>
<div class="editor-field">
<%: Html.DropDownList("State")%>
<%: Html.ValidationMessageFor(model => model.State) %>
</div>

Resources