MVC 4 binding nested list of lists result - asp.net-mvc

I've done some research on this and seem to find posts that are either outdated or don't quite work in my situation. I may be using wrong keywords when searching though... =/
On my web page I have Tabs containing Group boxes that contain Lines which in turn contain Items.
So it is lists of lists 4 levels.
The problem:
When posting back, ViewModel.Tabs is null and I can't save anything.
Everything displays quite nicely, but nothing is posted back.
The code
Views/Shared/EditorTemplates/Tab.cshtml
#model AWMCCRM.Web.ViewModels.Tab
#Html.HiddenFor(vm => vm.Name)
<div id="tab-#Model.Name.Replace(" ", string.Empty)" class="tab-content two">
#Html.EditorFor(vm => vm.Groups)
</div>
Views/Shared/EditorTemplates/Group.cshtml
#model AWMCCRM.Web.ViewModels.Group
#Html.HiddenFor(vm => vm.Name)
<fieldset>
<legend>#Model.Name</legend>
#Html.EditorFor(vm => vm.Lines)
</fieldset>
Views/Shared/EditorTemplates/Line.cshtml
#model AWMCCRM.Web.ViewModels.Line
<div class="_100Max">
#Html.HiddenFor(vm => vm.Name)
#Html.EditorFor(vm => vm.Items)
</div>
Views/Shared/EditorTemplates/Item.cshtml
#model AWMCCRM.Web.ViewModels.Item
<div class="_#Model.DisplaySize" title="#Model.Description">
<p>
#Html.HiddenFor(x => x.DataType)
#Html.HiddenFor(x => x.Description)
#Html.HiddenFor(x => x.DisplaySize)
#Html.HiddenFor(x => x.DisplayType)
#Html.HiddenFor(x => x.IDsPiped)
#Html.HiddenFor(x => x.ItemType)
#Html.HiddenFor(x => x.Name)
#Html.LabelFor(vm => vm.Value, Model.Name)
#switch (Model.DisplayType.ToLower().Replace(" ", string.Empty))
{
case "checkbox":
#Html.CheckBoxFor(vm => Convert.ToBoolean(vm.Value))
break;
case "dropdownlist":
#Html.DropDownListFor(vm => vm.Value, Model.ValueOptionListItems)
break;
case "multiselectlist":
#Html.ListBoxFor(
x => x.SelectedValueList,
Model.ValueOptionListItems,
new { id = "itemValuesMultiSelect", multiple = "multiple", Size = 15 })
break;
case "radiobutton":
#Html.RadioButtonFor(vm => vm.Value, Model.Value)
break;
case "textarea":
#Html.TextAreaFor(vm => vm.Value)
break;
default:
#Html.TextBoxFor(vm => vm.Value)
break;
}
</p>
</div>
The ViewModel (cut down version)
namespace AWMCCRM.Web.ViewModels
{
public class PersonEditViewModel
{
public List<Tab> Tabs { get; set; }
//Other properties
//...
}
}
The View (cut down version)
#using (Html.BeginForm("Edit", "Person", FormMethod.Post, new { id = "validate-form", #class = "block-content form" }))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(x => x.PersonID)
#foreach (var tab in Model.Tabs)
{
#Html.EditorFor(vm => tab)
}
<input class="close-toolbox button" type="submit" value="Save">
}
Any suggestions?
Thanks

http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
It's an old article, but it still applies.
I had this exact problem, but basically due to the model binding system you need to use an explicit for loop instead of a foreach loop, and reference your elements by their index.
#using (Html.BeginForm("Edit", "Person", FormMethod.Post, new { id = "validate-form", #class = "block-content form" }))
{
Html.AntiForgeryToken()
Html.HiddenFor(x => x.PersonID)
for (int i = 0; i<Model.Tabs.Count; i++)
{
Html.EditorFor(x => Model.Tabs[i])
}
<input class="close-toolbox button" type="submit" value="Save">
}

Related

#Html.HiddenFor returning null value

I am trying to return the results of a table back to the controller for further manipulation. Once returned to the controller the value shows as null. In the past I have been able to use #Html.HiddenFor to return the values but it doesn't seem to be working in this instance. Not sure what I am doing wrong here. Any help is greatly appreciated.
#model IEnumerable<Project.Models.Item>
#{
ViewBag.Title = "Welcome to The Project";
}
#using (Html.BeginForm("UpdateQuality", "Home", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
<div class="row">
<div class="form-group">
<table class="table table-bordered">
<tr>
<th>#Html.DisplayNameFor(m => m.Name)</th>
<th>#Html.DisplayNameFor(m => m.SellIn)</th>
<th>#Html.DisplayNameFor(m => m.Quality)</th>
</tr>
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>#Html.DisplayFor(m => m.ElementAt(i).Name)</td>
<td>#Html.DisplayFor(m => m.ElementAt(i).SellIn)</td>
<td>#Html.DisplayFor(m => m.ElementAt(i).Quality)</td>
#Html.HiddenFor(m => m.ElementAt(i).Name)
#Html.HiddenFor(m => m.ElementAt(i).SellIn)
#Html.HiddenFor(m => m.ElementAt(i).Quality)
</tr>
}
</table>
<div class="form-group">
<div style="margin-top: 50px">
<input type="submit" class="btn btn-primary" value="Advance Day"/>
</div>
</div>
</div>
</div>
}
And here is the controller which returns null.
public ActionResult UpdateQuality(List<Item> Items )
{
return View("Index", (object)Items);
}
You cannot use ElementAt() in a HtmlHelper method that generates form controls (look at the name attribute your generating - it does not match your model).
Either change the model to be IList<T>
#model List<Project.Models.Item>
and use a for loop
#for (int i = 0; i < Model.Count; i++)
{
....
#Html.HiddenFor(m => m.[i].Name)
....
or change use a custom EditorTemplate for typeof Item, and in the main view, use #Html.EditorFor(m => m) to generate the correct html for each item in the collection.

Using RadioButtonFor and CheckBoxFor in nested for loops (razor syntax)

My code looks like the following:
#for (var i = 0; i < Model.Count; i++)
{
<h2>Question #Html.DisplayFor(x => x[i].OrderIndex) </h2>
#Html.HiddenFor(x => x[i].SurveyId)
#Html.HiddenFor(x => x[i].QuestionId)
#Html.HiddenFor(x => x[i].AnswerId)
<br />
#Html.DisplayFor(x => x[i].QuestionText)
<br />
#Html.DisplayFor(x => x[i].QuestionText)
#if (Model[i].Type == QuestionType.Single)
{
<h2>Answer</h2>
#for (var ao = 0; ao < Model[i].Answer.AnswerOptions.Count; ao++)
{
#Html.RadioButtonFor(x => x[i].Answer.AnswerOptions[ao].IsSelectedAnswer, true) #Html.Raw(" ");
#Html.LabelFor(x => x[i].Answer.AnswerOptions[ao].IsSelectedAnswer, Model[i].Answer.AnswerOptions[ao].OptionText)<br />
#Model[i].Answer.AnswerOptions[ao].OptionText
}
}
}
I can get every other control type to work ok, but I'm not sure how to wire up radio buttons with my model. It's in a nested for loop.
I've also tried:
#Html.RadioButtonFor(x => x[i].Answer.AnswerOptions[ao].IsSelectedAnswer, Model[i].Answer.AnswerOptions[ao].OptionText)
#Html.Raw(" ");
This should get the value, but it does not. Also, I can't get displayfor or labelfor working:
#Html.LabelFor(x => x[i].Answer.AnswerOptions[ao].IsSelectedAnswer, Model[i].Answer.AnswerOptions[ao].OptionText)
<br />

Send single value through RouteValueDictionary

One of the properties of my ViewModel is an array which, unfortunately, is null every time I post back to the controller. I figured a simple hack where I place the values into a coma-delimited string.
This works great for our paging plugin, which posts back to our Index method, using a RouteValueDictionary. However, it is not working in the Html.BeginForm helper which posts back to a different controller action (the Update method).
View
#*Since we can't send arrays or complex objects break array down into string for RouteValueDictionary*#
var channelCodes = "";
for (int i = 0; i < Model.searchChannelCode.Length; i++)
{
channelCodes += Model.searchChannelCode[i];
if (i + 1 < Model.searchChannelCode.Length)
{
channelCodes += ",";
}
}
#*The 'searchChannelCodesPagin' variable from this RouteValueDictionary always posts back as null
using (Html.BeginForm("Update", "ZipCodeTerritory", new RouteValueDictionary()
{
{"searchChannelCodesPaging", channelCodes }
}, FormMethod.Post, new {id = "UpdateForm"}))
{
#Html.HiddenFor(model => model.searchZip)
#Html.HiddenFor(model => model.searchTerritory)
#Html.HiddenFor(model => model.searchState)
#Html.HiddenFor(model => model.searchActiveOnly)
#Html.HiddenFor(model => model.zipCodeTerritory)
<div id="cloneBox">
<div id="rw1">
#Html.LabelFor(model => model.newTerritory)
#Html.TextBoxFor(model => model.newTerritory, new { style = "width: 30px;padding-left:10px;", maxLength = 3 })
#Html.LabelFor(model => model.newDescription)
#Html.TextBoxFor(model => model.newDescription, new { style = "width: 250px;padding-left:10px;", maxLength = 30 })
#Html.LabelFor(model => model.newEffectiveDate)
#Html.TextBoxFor(model => model.newEffectiveDate, new { style = "width: 80px;padding-left:10px;" })
<div id="rw2" style="padding-top: 10px;">
#Html.LabelFor(model => model.newChannelCode)
#Html.DropDownListFor(model => model.newChannelCode, Model.ChannelCodes, " ")
#Html.LabelFor(model => model.newStateCode)
#Html.DropDownListFor(model => model.newStateCode, Model.StateCodes, " ")
#Html.LabelFor(model => model.newEndDate)
#Html.TextBoxFor(model => model.newEndDate, new { style = "width: 80px;" })
</div>
</div>
</div>
<br/>
<div id="buttonDiv">
<button type="submit" id="CloneButton" name="button" value="clone">Apply New Data</button>
<button type="submit" id="deleteButton" name="button" value="delete">Delete Selected Items</button>
<button type="submit" id="removeButton" name="button" value="removeErrors">Remove Selected Errors</button>
</div>
}
Controller
The forma above posts to this controller action. The searchChannelCodePaging variable is null each time.
[HttpPost]
public ActionResult Update(ZipCodeIndex updateZip, string button, string searchChannelCodesPaging)
{
Since you are doing a post, the simplest way to get it to the backend would be add a hidden field:
#Html.HiddenFor("searchChannelCodesPaging", searchChannelCodesPaging);
As a routing value, you may need to get it explicitly within the control via one of the two following approaches. These objects are directly accessible within the Controller class.
RouteData.Values("searchChannelCodesPaging")
Request.QueryString.Get("searchChannelCodesPaging");
You don't have to serialize a array type model parameter to a CSV string to get it to post to your controller. You can do this instead:
#for (var i = 0; i < Model.searchChannelCode.Length; i++)
{
#Html.HiddenFor(m => m.searchChannelCode[i]);
}

Partial View models

Within my layout page, I want to either show a Login box, OR the details about the logged in person.
<div>
#if (Request.IsAuthenticated)
{
#Html.ViewBag.UserDisplay
#Html.ActionLink("[Logout]", "LogoutUser", "User")
}
else
{
#Html.Partial("_Login")
}
<hr />
#RenderBody()
</div>
My login partial view:
#model BasicFinanceUI.Models.LoginModel
#using (Html.BeginForm("LoginUser", "User"))
{
#Html.ValidationSummary()
<p>
Username: #Html.TextBoxFor(x => x.Username)
Password: #Html.TextBoxFor(x => x.Password)
Remember Me: #Html.DropDownListFor(x => x.RememberMe, new[]
{
new SelectListItem() { Text = "Yes", Value = "true"},
new SelectListItem() {Text = "No", Value = "false"}
}, "Select")
<input type="submit" value="Login" />
#Html.ActionLink("[Register]", "Register", "User")
</p>
}
When I load the screen, and login, it works fine.
However, when I click 'Register', it loads the register screen. The login box is still visible in the Layout.
My registration screen is displayed:
#model BasicFinanceUI.Models.RegisterationModel
#{
ViewBag.Title = "Register";
Layout = "~/Views/Shared/SiteLayout.cshtml";
}
<h2>Register</h2>
#using (Html.BeginForm("Register", "User"))
{
<p>#Html.ValidationSummary()</p>
<p>Username: #Html.TextBoxFor(x => x.Username)</p>
<p>Password: #Html.PasswordFor(x => x.Password1)</p>
<p>Retype Password: #Html.PasswordFor(x => x.Password2)</p>
<p>Firstname: #Html.TextBoxFor(x => x.Firstname)</p>
<p>Surname: #Html.TextBoxFor(x => x.Surname)</p>
<p>Email: #Html.TextBoxFor(x => x.Email)</p>
<p><input type="submit" value="Register"/></p>
}
When I click the Register button, things go wrong.
I get the error:
The model item passed into the dictionary is of type
'BasicFinanceUI.Models.RegisterationModel', but this dictionary
requires a model item of type 'BasicFinanceUI.Models.LoginModel'.
It seems because there are two forms on the screen, they're getting mixed up. What am I doing wrong?

asp net mvc partial view validation

Hi how are you? I'm trying to validate a form in ASP NET MVC.
I have a partial view "Address" that I reuse for some entities, like Company, Person, etc.
My problem is that when I submit the form, only the controls of the father view gets validated, the ones in the partial view don't.
Here's some code I hope u can geive me hand
PERSON VIEW
#model Entities.Person
#using (Html.BeginForm("Create", "Person", FormMethod.Post))
{
<table>
<tr>
<td>
#Html.LabelFor(model => model.FirstName)
<div class="control">
#Html.TextBoxFor(model => model.FirstName, new { #maxlength = 7, #class = "numeric"})
#Html.ValidationMessageFor(model => model.FirstName)
</div>
<div class="spacer-short"></div>
</td>
<td>
#Html.LabelFor(model => model.LastName)
<div class="control">
#Html.TextBoxFor(model => model.LastName, new { #maxlength = 7, #class = "numeric"})
#Html.ValidationMessageFor(model => model.LastName)
</div>
<div class="spacer-short"></div>
</td>
</tr>
</table>
#{ Html.RenderAction("Index", "Address", new {id = Model.AddressId});} //Renders the Address form part
<div class="spacer"></div>
<button type="submit" data-button="true" data-button-icon="ui-icon-disk" class="">Create</button>
<button type="button" data-button="true" data-button-icon="ui-icon-circle-close" class="button-cancel">Cancel</button>
}
ADDRESS VIEW
#model Entities.Address
<table>
<tr>
<td>
#Html.LabelFor(model => model.Street)
<div class="control">
#Html.TextBox("Address.Street", Model.Street)
#Html.ValidationMessage("Address.Street")
</div>
<div class="spacer-short"></div>
</td>
<td>
#Html.LabelFor(model => model.Number)
<div class="control">
#Html.TextBox("Address.Number", Model.Number)
#Html.ValidationMessage("Address.Number")
</div>
<div class="spacer-short"></div>
</td>
</tr>
</table>
Then I have some validation metadata ([Required]) for person and address fields
A common mistake using #Html.Partial(...) and expecting validation errors to show up is not passing ViewData to that partial.:
#Html.Partial([ViewName], [EmailAddressObject], ViewData)
Try this:
#Html.EditorFor(Model.Street)
#Html.ValidationMessageFor(Model.Street)
instead of this:
#Html.TextBox("Address.Street", Model.Street)
#Html.ValidationMessage("Address.Street")
Try using Html.Partial(...) instead of Html.RenderAction(...) to render the partial view. This may be suppressing the validations.
Model
İmportant: Manage Nuget Packages
=>> JQuery.Validation
=>> Microsoft.JQueryUnobtrusive.Validation
install.
1) First step
using System.ComponentModel.DataAnnotations;
2)
public int Id { get; set; }
[Required(ErrorMessage = "* Kategori adı boş geçilemez.")]
[DisplayName("Kategori Adı")]
public string CategoryName { get; set; }
[Required(ErrorMessage = "* Kategori açıklaması boş geçilemez.")]
[DisplayName("Kategori Açıklaması")]
public string Description { get; set; }
3)
if (model != null && ModelState.IsValid)
{
var categoryCreate = new Categories
{
//code
};
_CategoriesService.Create(categoryCreate);
}
4)
Add ViewModel
#model Models.CategoryViewModel
After
#Html.TextBoxFor(model => model.CategoryName, new { #class = "form-control input-sm", id = "categoryTitle", #placeholder = "Kategori adını giriniz.", #type= "text", #required=true })
#Html.ValidationMessageFor(model => model.CategoryName, "", new { #class = "error-message" })
#Html.TextBoxFor(model => model.Description, new { #class = "form-control input-sm", id = "categoryArticle", #placeholder = "Kategori açıklamasını giriniz.", #type = "text", #required = true })
#Html.ValidationMessageFor(model => model.Description, "", new { #class = "error-message" })
In my case, I was missing
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
Once I put that in my _layout.cshtml file, it started working even with partial views.

Resources