Model binding with complex type - asp.net-mvc

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.

Related

allow to select only one radiobutton

I have a survey with 6 questions. Each question will be rated between (1-5). I want to capture the value selected for each question. Only one value can be selected on each question
The following is allowing me to select more than one option for each question, I should restrict to select only one for each question. All the radio buttons should be grouped as one, and only one button can be selected. I should be able to capture the selected value for each when submitted.
public class SurveyViewModel
{
public GroupViewModel GroupA { get; set; }
public GroupViewModel GroupB { get; set; }
public GroupViewModel GroupC { get; set; }
public SurveyViewModel()
{
GroupA = new GroupViewModel();
GroupA.GroupName = "A";
GroupB = new GroupViewModel();
GroupB.GroupName = "B";
GroupC = new GroupViewModel();
GroupC.GroupName = "C";
}
}
public class GroupViewModel
{
public string GroupName { get; set; }
public bool radio1 { get; set; }
public bool radio2 { get; set; }
public bool radio3 { get; set; }
public bool radio4 { get; set; }
public bool radio5 { get; set; }
}
Editor Template:
<div>
<h4>GroupViewModel</h4>
<hr />
<dl class="dl-horizontal">
<dt>#Html.RadioButtonFor(model => model.radio1, true, new { id = Model.GroupName + "1" })</dt>
<dd>#Html.DisplayNameFor(model => model.radio1)</dd>
<dt>#Html.RadioButtonFor(model => model.radio2, true, new { id = Model.GroupName + "2" })</dt>
<dd>#Html.DisplayNameFor(model => model.radio2)</dd>
<dt>#Html.RadioButtonFor(model => model.radio3, true, new { id = Model.GroupName + "3" })</dt>
<dd>#Html.DisplayNameFor(model => model.radio3)</dd>
<dt>#Html.RadioButtonFor(model => model.radio4, true, new { id = Model.GroupName + "4" })</dt>
<dd>#Html.DisplayNameFor(model => model.radio4)</dd>
<dt>#Html.RadioButtonFor(model => model.radio5, true, new { id = Model.GroupName + "5" })</dt>
<dd>#Html.DisplayNameFor(model => model.radio5)</dd>
</dl>
</div>
and the survey View as follows:
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.GroupA)
#Html.EditorFor(m => m.GroupB)
#Html.EditorFor(m => m.GroupC)
<input type="submit" value="Continue" />
}
I have added a group name as posted below, but I could not able to capture the selected value in the http post
#Html.RadioButtonFor(model => model.radio1, true, new { id = Model.GroupName + "1", #Name = Model.GroupName })
You have to override the name attribute:
#Html.RadioButtonFor(model => model.radio1, true, new { id = Model.GroupName + "1", #Name = "Group" })
You have to do this for every radiobutton that you want to have only one selection.
When you use Html.RadioButtonFor it expected that you use the same property to bind all radio buttons like:
#Html.RadioButtonFor(model => model.radio, "answer1")
#Html.RadioButtonFor(model => model.radio, "answer2")
#Html.RadioButtonFor(model => model.radio, "answer3")
Or using an Enumeration in place of "answer1", "answer2" and "answer3"
Each radio button you generating if for a different property and has a different name (they are not grouped) so all radio buttons within a 'Question' can be selected, and worse, none of then can be unselected once selected.
In addition your properties within SurveyViewModel are all the same type, so you should be using a collection property, not generating individual properties for each item.
Start by creating a view model which represents what you want to display/edit in the view.
public class QuestionVM
{
public string Name { get; set; }
[Required(ErrorMesage = "Please select a rating")]
public int? Rating { get; set; }
}
and in the controller GET method
List<QuestionVM> model = new List<QuestionVM>()
{
new QuestionVM() { Name = "A" },
new QuestionVM() { Name = "B" },
new QuestionVM() { Name = "C", Rating = 3 } // use this if you want to pre-select a radio button
};
return View(model);
Next, create an EditorTemplate for QuestionVM. The templates needs to be named /Views/Shared/EditorTemplates/QuestionVM.cshtml
#model QuestionVM
<div class="question">
#Html.HiddenFor(m => m.Name) // so this value posts back
<h2>#Model.Name</h2>
#for(int i = 1; i < 6; i++)
{
<label>
#Html.RadioButtonFor(m => m.Rating, i, new { id = "" })// remove id attribute to prevent invalid html
<span>#i</span>
</label>
}
#Html.ValidationMessageFor(m => m.Rating)
</div>
and finally in the main view
#model IEnumerable<QuestionVM>
#using (Html.BeginForm())
{
#Html.EditorFor(m => m)
<input type="submit" value="Save" />
}
which will post back to
public ActionResult Edit(IEnumerable<QuestionVM model) // adjust method name to suit

Passing Model data from View to Controller

I am trying to pass the Model data from a View (and PartialView within the View) back to the Controller upon HttpPost. (Adapted from Pass SelectedValue of DropDownList in Html.BeginForm() in ASP.NEt MVC 3)
Why? I want to show a list of assets each with a DropDownList and number of options. Upon submission of form to read the selected items from DropDownList.
My 2 (simplified) models:
public class Booking
{
public int BookingID { get; set; }
public int StoreID { get; set; }
...
public IEnumerable<AssetShort> Assets { get; set; }
}
and
public class AssetShort
{
public int AssetID { get; set; }
....
public int SelectedAction { get; set; }
public IEnumerable<SelectListItem> ActionList { get; set; }
}
In my Booking Controller > Create I build the List:
public ActionResult Booking(int id)
{
// get myBag which contains a List<Asset>
// booking corresponds to 'id'
var myAssets = new List<AssetShort>();
foreach (var a in myBag.Assets)
{
var b = new AssetShort();
b.AssetID = a.ID;
b.SelectedAction = 0;
b.ActionList = new[]
{
new SelectListItem { Selected = true, Value = "0", Text = "Select..."},
new SelectListItem { Selected = false, Value = "1", Text = "Add"},
new SelectListItem { Selected = false, Value = "2", Text = "Remove"},
new SelectListItem { Selected = false, Value = "3", Text = "Relocate"},
new SelectListItem { Selected = false, Value = "4", Text = "Upgrade"},
new SelectListItem { Selected = false, Value = "5", Text = "Downgrade"}
};
myAssets.Add(b);
};
var model = new BookingRequirementsViewModel
{
BookingID = booking.ID,
StoreID = booking.StoreID,
Assets = myAssets.ToList(),
};
return View(model);
My View:
#model uatlab.ViewModels.BookingRequirementsViewModel
#{
ViewBag.Title = "Booking step 2";
}
<h4>Your booking ref. #Model.BookingID</h4>
#using (Html.BeginForm("Booking2", "Booking", FormMethod.Post))
{
<fieldset>
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.StoreID)
#Html.Partial("_Assets", Model.StoreAssets)
<input type="submit" value="Cancel" class="btn btn-default" />
<input type="submit" value="Next" class="btn btn-default" />
</fieldset>
}
The Partial View includes
#foreach (var item in Model)
{
<tr>
<td>#item.Name</td>
<td>#item.Number</td>
<td>#Html.DropDownListFor(modelItem=>item.SelectedAction, item.ActionList)</td>
</tr>
}
So, all this works fine in the browser and I can select dropdowns for each asset listed but when I submit the only value posted back is the StoreID as it is in a "HiddenFor".
The booking2 controller has the model for a parameter:
public ActionResult Booking2(BookingRequirementsViewModel model)
{
//loop through model.Assets and display SelectedActions
}
Let me make it clear what the problems is - in Booking2 controller the Model is null when viewed in Debug mode and I get error "Object reference not set to an instance of an object."
Any ideas please how to pass back the Model to controller from view?
Regards
Craig
You need to create an EditorTemplate for AssetShort. I also suggest moving ActionList to the BookingRequirementsViewModel so your not regenerating a new SelectList for each AssetShort
The models you have posted aren't making sense. Your controller has var model = new BookingRequirementsViewModel { ..., Assets = myAssets.ToList() }; but in the view you refer to #Html.Partial("_Assets", Model.StoreAssets)? Are these 2 different properties. I will assume that StoreAssets is IEnumerable<AssetShort>
/Views/Shared/EditorTemplates/AssetShort.cshtml
#model AssetShort
<tr>
<td>#Html.DispayFor(m => m.Name)</td>
....
<td>
#Html.DropDownListFor(m => m.SelectedAction, (IEnumerable<SelectListItem>)ViewData["actionList"], "--Please select--")
#Html.ValidationMessageFor(m => m.SelectedAction)
</td>
</tr>
In the main view
#model uatlab.ViewModels.BookingRequirementsViewModel
....
#using (Html.BeginForm()) // Not sure why you post to a method with a different name
{
....
#Html.HiddenFor(m => m.StoreID)
#Html.EditorFor(m => m.StoreAssets, new { actionList = Model.ActionList })
....
}
In the controller
public ActionResult Booking(int id)
{
....
var model = new BookingRequirementsViewModel
{
BookingID = booking.ID,
StoreID = booking.StoreID,
Assets = myBag.Assets.Select(a => new AssetShort()
{
AssetID = a.ID,
SelectedAction = a.SelectedAction, // assign this if you want a selected option, otherwise the "--Please select--" option will be selected
....
})
};
ConfigureViewModel(model); // Assign select list
return View(model);
}
And a separate method to generate the SelectList because it needs to be called in the GET method and again in the POST method if you return the view. Note use the overload of DropDownListFor() to generate the option label (null value) as above, and there is no point setting the Selected property (the value of SelectedAction determines what is selected, not this)
private ConfigureViewModel(BookingRequirementsViewModel model)
{
model.ActionList = new[]
{
new SelectListItem { Value = "1", Text = "Add"},
....
new SelectListItem { Value = "5", Text = "Downgrade"}
};
}
and the POST
public ActionResult Booking(BookingRequirementsViewModel model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model); // Re-assign select list
return View(model);
}
// save and redirect
}
I recommend also making SelectedAction nullable with the [Required] attribute so you get client and server side validation
public class AssetShort
{
public int AssetID { get; set; }
....
[Required]
public int? SelectedAction { get; set; }
}

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

MVC trying to pass model from razor view to controller

So my story is that I am having trouble with the post to the controller, the view seems to work fine. When the postback happens the tm.BookId is 0 (should be 1) and the list count is 0. First I will display the model:
public class TransferModel
{
public TransferModel()
{
cbItems = new List<CheckBoxItem>();
}
public List<CheckBoxItem> cbItems {get;set;}
public int BookId;
public class CheckBoxItem
{
public int AttributeId { get; set; }
public string Attribute { get; set; }
public bool Selected { get; set; }
}
}
The Controller part:
public ActionResult AddAttributes(int id = 0)
{
db.transMod.BookId = id;
BookInfo book = db.BookInfoes.Find(id);
var latts = db.BookAtts.ToList();
foreach (BookAtt ba in latts)
{
db.transMod.cbItems.Add(new TransferModel.CheckBoxItem { Attribute = ba.Attribute, AttributeId = ba.BookAttId, Selected = false });
}
List<BookAtt> atInList = book.BookAtts.ToList();
foreach (TransferModel.CheckBoxItem cb in db.transMod.cbItems)
{
if (atInList.Exists(item => item.Attribute == cb.Attribute))
cb.Selected = true;
}
return View(db.transMod);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddAttributes(TransferModel tm)
{
List<BookAtt> atPool = db.BookAtts.ToList();
BookInfo book = db.BookInfoes.Find(tm.BookId);
foreach (TransferModel.CheckBoxItem sel in tm.cbItems)
{
if (sel.Selected)
book.BookAtts.Add(atPool.Find(item1 => item1.Attribute == sel.Attribute));
}
db.SaveChanges();
return RedirectToAction("AddAttributes");
}`enter code here`
And finally the view:
#model BrightStar.Models.TransferModel
#{
ViewBag.Title = "Update Attributes";
}
<h2>Add Attributes</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
<table>
#Html.HiddenFor(model => Model.BookId)
#Html.HiddenFor(model => Model.cbItems)
#foreach (var itm in Model.cbItems)
{
<tr>
<td>#Html.HiddenFor(mo => itm.AttributeId)</td>
<td>#Html.CheckBoxFor(mo => itm.Selected)</td>
<td>#Html.DisplayFor(mo => itm.Attribute)</td>
</tr>
}
</table>
<p>
<input type="submit" value="Save" />
</p>
}
enter code here
Model binding doesn't happen automatically, items needs to be in certain format to get binded to list properties in POST actions. Check this out.
Try checking out the value of BookId property in the DOM to confirm it is 1, otherwise it should bind normally.
You should reference your model's properties in helpers to correctly generate names for your controls:
#Html.HiddenFor(model => Model.cbItems)
should be
#Html.HiddenFor(model => model.cbItems)

MVC - Problem with DataBinding to a Collection using a Custom Template

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);
}
}

Resources