ViewModel inside of ViewModel - How to get it to post? - c#

I have a viewmodel inside of another viewmodel for seperation of concerns. I created an editor template for it and set up the default values in the controller at runtime. Unfortunately when the parent view model posts to the controller, it does not save the values of the child view models' items. Here is the code:
Note: Some code names were changed, so if there's any incongruities please point it out in the comment. I've gone over it about 4x and found them all I think.
public class ParentViewModel {
public ChildViewModel {get;set;}
}
public class ChildViewModel {
public List<Item> Items {get;set;}
}
public class Item {
public int Id {get;set;
public string Name {get;set;}
}
I've created an EditorTemplate that binds properly on the view
#model MyProject.ViewModels.ChildViewModel
#foreach (var item in Model.Items)
{
<div class="Item" #String.Format("id=Item{0}", #item.Id) >
Item ##Html.DisplayFor(models => item.Id):
#Html.LabelFor(model => item.Name)
#Html.EditorFor(model => item.Name)
</div>
}
However, when I submit the form that the ParentViewModel is bound to, the ChildViewModel's items are null!
Controller.cs
public class ControllerController{
public ActionResult Form {
return View(new ParentViewModel {
ChildViewModel = new ChildViewModel {
Items = new List<Item>(Enumerable.Range(1,20).Select(i => new Item { Id=i })
}
});
}
[HttpPost]
[ActionName("Form")]
public class ActionResult FormSubmitted(ParentViewModel parentViewModel) {
//parentViewModel.ChildViewModel.Items is null!
_fieldThatIsRepresentingMyDataService.Save(parentViewModel);
}
}
ViewView.cshtml
<div class="editor-label">
#Html.LabelFor(model => model.ChildViewModel)
</div>
<div id="ItemList" class="editor-field">
#Html.EditorFor(model => model.ChildViewModel)
</div>
Any help is greatly appreciated.

The problem is not with nested viewmodels, but the way model binding works with forms and arrays.
You need to ensure that your form items render like this:
<input type="text" name="people[0].FirstName" value="George" />
<input type="text" name="people[0].LastName" value="Washington" />
<input type="text" name="people[1].FirstName" value="Abraham" />
<input type="text" name="people[1].LastName" value="Lincoln" />
<input type="text" name="people[3].FirstName" value="Thomas" />
<input type="text" name="people[3].LastName" value="Jefferson" />
The key part is the array index in input's name attribute. Without the index part, Model Binding will not populate your List.
And to render that, you need a for loop:
#for (int i = 0; i < Model.Items.Length; i++) {
...
#Html.EditorFor(m => Model.Items[i].Name)
...
}
Check out this post from Phil Haack that talks about the it in details.

Related

How to bind list<object> to a model and submit along with other form data?

This is what I'm trying to do:
I have a form with some input fields. Part of the form allows user to choose multiple options (Books) but will not know how many.
Model:
public class Data
{
public string name { get; set; }
public string age { get; set; }
...
public List<Books> books { get; set; }
...
}
And,
public class Books
{
public string Title { get; set; }
public string Author { get; set; }
}
View:
#model Applicants.Models.Data
...
<input type="text" name="Title" value="" />
<input type="text" name="Author" value="" />
My question is, how do I submit multiple titles and authors along with other form data? And how to properly name the input fields?
I have read this https://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
But the example only submitted a List, not with other data.
Thanks.
Since you're using Razor view, you can use strongly-typed #Html.TextBoxFor() helpers and use for loop to generate multiple textboxes inside books list:
#model Applicants.Models.Data
#using (Html.BeginForm())
{
#Html.TextBoxFor(model => model.name)
#Html.TextBoxFor(model => model.age)
#for (int i = 0; i < Model.books.Count; i++)
{
#Html.TextBoxFor(model => model.books[i].Title)
#Html.TextBoxFor(model => model.books[i].Author)
}
#* don't forget to add submit button here *#
}
The loop will produce <input> elements like this example below, assumed that you have POST action which has Applicants.Models.Data as viewmodel parameter:
<input name="books[0].Title" type="text" value="sometitle">
<input name="books[0].Author" type="text" value="somevalue">
<input name="books[1].Title" type="text" value="sometitle">
<input name="books[1].Author" type="text" value="somevalue">
<!-- other input elements depending on books list count -->
Refer to this fiddle for working example.
you can do like this for bind model for normal html syntax its just example
you need modified according to your need
<input type="text" name="Model.name" value="Curious George" />
<input type="text" name="Model.age" value="H.A. Rey" />
<input type="text" name="Model.books[0].Title" value="Curious George" />
<input type="text" name="Model.books[0].Author" value="H.A. Rey" />

How can I pass hidden field value from view to controller ASP.NET MVC 5?

I am trying to pass hidden field value from view to controller by doing the following
#Html.HiddenFor(model => model.Articles.ArticleId)
and also tried
<input type="hidden" id="ArticleId" name="ArticleId" value="#Model.Articles.ArticleId" />
On both instances the value of ArticleId is 0 but when i use TextboxFor i can see the correct ArticleId, please help
Here it is
View
#model ArticlesCommentsViewModel
....
#using (Html.BeginForm("Create", "Comments", FormMethod.Post))
{
<div class="row">
<div class="col-xs-10 col-md-10 col-sm-10">
<div class="form-group">
#Html.LabelFor(model => model.Comments.Comment, new { #class = "control-label" })
#Html.TextAreaFor(m => m.Comments.Comment, new { #class = "ckeditor" })
#Html.ValidationMessageFor(m => m.Comments.Comment, null, new { #class = "text-danger"})
</div>
</div>
</div>
<div class="row">
#*#Html.HiddenFor(model => model.Articles.ArticleId)*#
<input type="hidden" id="ArticleId" name="ArticleId" value="#Model.Articles.ArticleId" />
</div>
<div class="row">
<div class="col-xs-4 col-md-4 col-sm-4">
<div class="form-group">
<input type="submit" value="Post Comment" class="btn btn-primary" />
</div>
</div>
</div>
}
Controller
// POST: Comments/Create
[HttpPost]
public ActionResult Create(CommentsViewModel comments)//, int ArticleId)
{
var comment = new Comments
{
Comment = Server.HtmlEncode(comments.Comment),
ArticleId = comments.ArticleId,
CommentByUserId = User.Identity.GetUserId()
};
}
Model
public class CommentsViewModel
{
[Required(ErrorMessage = "Comment is required")]
[DataType(DataType.MultilineText)]
[Display(Name = "Comment")]
[AllowHtml]
public string Comment { get; set; }
public int ArticleId { get; set; }
}
ViewModel
public class ArticlesCommentsViewModel
{
public Articles Articles { get; set; }
public CommentsViewModel Comments { get; set; }
}
The model in the view is ArticlesCommentsViewModel so therefore the parameter in your POST method must match. Your use of
#Html.HiddenFor(model => model.Articles.ArticleId)
is correct, but you need to change the method to
[HttpPost]
public ActionResult Create(ArticlesCommentsViewModel model)
and the model will be correctly bound.
As a side note, your ArticlesCommentsViewModel should not contain data models, and instead should contain only those properties you need in the view. If typeof Articles contains properties with validation attributes, ModelState would be invalid because your not posting all properties of Article.
However, since CommentsViewModel already contains a property for ArticleId, then you could just use
#Html.HiddenFor(model => model.Comments.ArticleId)
and in the POST method
[HttpPost]
public ActionResult Create([Bind(Prefix="Comments")]CommentsViewModel model)
to effectively strip the "Comments" prefix
In your controller, you need to pass the hidden value with the model,
for example, if you have a userId as a hidden value, in your Page you add:
#Html.HiddenFor(x => x.UserId)
In your model of course you would already have UserId as well.
In your controller, you need the model as a parameter.
public async Task<ActionResult> ControllerMethod(YourViewmodel model) { model.UserId //this should be your HiddenValue
I guess your model have another class called Articles inside CommentsViewModel.Change your controller function for accessing the ArticleId accordingly.
[HttpPost]
public ActionResult Create(CommentsViewModel comments)//, int ArticleId)
{
var comment = new Comments
{
Comment = Server.HtmlEncode(comments.Comment),
ArticleId = comments.Articles.ArticleId, // Since you are using model.Articles.ArticleId in view
CommentByUserId = User.Identity.GetUserId()
};
}
In my case, I didn't put the hidden input in the form section, but out of form, so it's not send to backend. Make sure put hidden input inside the form.
Also make sure name attribute is specified on the hidden field. Element's "id" is often used on client side but "name" on server side.
<input type="hidden" value="#ViewBag.selectedTraining" id="selectedTraining"
name="selectedTraining" />
In my case, I was passing a couple of fields back and forth between controllers and views. So I made use of hidden fields in the views.
Here's part of the view. Note a controller had set "selectedTraining" and "selectedTrainingType" in the ViewBag to pass to the view. So I want these values available to pass on to a controller. On the hidden tag, the critical thing is set to the "name" attribute. "id" won't do it for you.
#using (Html.BeginForm("Index", "ComplianceDashboard"))
{
<input type="hidden" value="#ViewBag.selectedTraining" id="selectedTraining" name="selectedTraining" />
<input type="hidden" value="#ViewBag.selectedTrainingType" id="selectedTrainingType" name="selectedTrainingType" />
if (System.Web.HttpContext.Current.Session["Dashboard"] != null)
{
// Show Export to Excel button only if there are search results
<input type="submit" id="toexcel" name="btnExcel" value="Export To Excel" class="fright" />
}
<div id="mainDiv" class="table">
#Html.Grid(Model).Columns(columns =>
Then back on the controller:
// POST: Dashboard (Index)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(string excel)
{
string selectedTraining, selectedTrainingType;
selectedTraining = Request["selectedTraining"];
selectedTrainingType = Request["selectedTrainingType"];
Or can put the requests as parameters to the method: public ActionResult Index(string excel, string selectedTraining, string selectedTrainingType)

Create more than one object of the same type in the same view

in my create view I want to give the user the possibility to create a list of objects (of the same type). Therefore I created a table in the view including each inputfield in each row. The number of rows respective "creatable" objects is a fixed number.
Lets say there is a class Book including two properties title and author and the user should be able two create 10 or less books.
How can I do that?
I don't know how to pass a list of objects (that are binded) to the controller. I tried:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ICollection<Book> bookList)
{
if (ModelState.IsValid)
{
foreach(var item in bookList)
db.Books.Add(item);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(articlediscounts);
}
And in the view it is:
<fieldset>
<legend>Book</legend>
<table id="tableBooks" class="display" cellspacing="0" width="100%">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < 10 ;i++ )
{
<tr>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
</td>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Author)
#Html.ValidationMessageFor(model => model.Author)
</div>
</td>
</tr>
}
</tbody>
</table>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
As booklist is null, it doesn't work and I don't know how to put all created objects in this list.
If you have any suggestions I would be very thankful.
Scott Hanselman has some details on passing arrays to MVC control binding: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Which is essentially: ensure your controls have the correct names: using an index for lists
Change your for loop to something like:
#for (int i = 0; i < 10 ; i++)
{
<tr>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Title" />
</div>
</td>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Author" />
</div>
</td>
</tr>
}
this will then bind to your post action automatically.
[HttpPost]
public ActionResult Create(IList<Book> bookList)
You can then show/hide these as required or use js/jquery to add them dynamically
Edit: As correctly observed by Stephen Muecke, the above answer only regards the binding from the form+fields to the HttpPost, which appears to be the emphasis of the question.
The post action in the original post is not compatible with the view. There's quite a bit of missing code in the OP that may or may not be relevant, but worth observing that if your view is for a single model, then your fail code on ModelState.IsValid needs to return a single model or your view needs to be for an IList (or similar), otherwise you won't get server-side validation (but you can still get client-side validation if you manually add it to the <input>s)
The fact you use #Html.EditorFor(model => model.Title) suggests that you have declared the model in the view as
#model yourAssembly.Book
Which allows to to post back only one Book so the POST method would need to be
public ActionResult Create(Book model)
Note that you current implementation create inputs that look like
<input id="Title" name="Title" ... />
The name attributes do not have indexers (they would need to be name="[0].Title", name="[1].Title" etc.) so cannot bind to a collection, and its also invalid html because of the duplicate id attributes.
If you want to create exactly 10 books, then you need initialize a collection in the GET method and pass the collection to the view
public ActionResult Create()
{
List<Book> model = new List<Book>();
for(int i = 0; i < 10;i++)
{
model.Add(new Book());
}
return View(model);
}
and in the view
#model yourAssembly.Book
#using (Html.BeginForm())
{
for(int i = 0; i < Model.Count; i++)
{
#Html.TextBoxFor(m => m[i].Title)
#Html.ValidationMessageFor(m => m[i].Title)
.... // ditto for other properties of Book
}
<input type="submit" .. />
}
which will now bind to your collection when you POST to
public ActionResult Create(List<Book> bookList)
Note the collection should be List<Book> in case you need to return the view.
However this may force the user to create all 10 books, otherwise validation may fail (as suggested by your use of #Html.ValidationMessageFor()). A better approach is to dynamically add new Book items in the view using either the BeginCollectionItem helper method (refer example) or a client template as per this answer.
You'd need to send a JSON object that has the list of books in it. So the first thing is to create a Model class like this:
public class SavedBooks{
public List<Book> Books { get; set; }
}
Then the Book class would have to have these 2 props:
public class Book {
public string Title { get; set; }
public string Author { get; set; }
}
Next, change your controller to use this model:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SavedBooks model)
Then create a javascript method (using jQuery) to create a JSON object that matches the structure of the controllers SavedBooks class:
var json = { Books: [ { Title: $('#title_1').val(), Author: $('#Author_1').val() } ,
{ as many items as you want }
]
};
$.ajax(
{
url: "/Controller/Create",
type: "POST",
dataType: "json",
data: json
});

MVC Form not able to post List of objects

so I have an MVC Asp.net app that is having issues. Essentially, I have a View that contains a form, and its contents are bound to a list of objects. Within this loop, it loads PartialView's with the items being looped over. Now everything works up till the submittion of the form. When it gets submitted, the controller is sent a null list of objects. The code below demonstates the problems.
Parent View:
#model IEnumerable<PlanCompareViewModel>
#using (Html.BeginForm("ComparePlans", "Plans", FormMethod.Post, new { id = "compareForm" }))
{
<div>
#foreach (var planVM in Model)
{
#Html.Partial("_partialView", planVM)
}
</div>
}
_partialView:
#model PlanCompareViewModel
<div>
#Html.HiddenFor(p => p.PlanID)
#Html.HiddenFor(p => p.CurrentPlan)
#Html.CheckBoxFor(p => p.ShouldCompare)
<input type="submit" value="Compare"/>
</div>
And these are the classes for the above code:
PlanViewModel:
public class PlansCompareViewModel
{
public int PlanID { get; set; }
public Plan CurrentPlan { get; set; }
public bool ShouldCompare { get; set; }
public PlansCompareViewModel(Plan plan)
{
ShouldCompare = false;
PlanID = plan.PlanId;
CurrentPlan = plan;
}
public PlansCompareViewModel()
{
// TODO: Complete member initialization
}
public static IEnumerable<PlansCompareViewModel> CreatePlansVM(IEnumerable<Plan> plans)
{
return plans.Select(p => new PlansCompareViewModel(p)).AsEnumerable();
}
}
Controller:
public class PlansController : MyBaseController
{
[HttpPost]
public ActionResult ComparePlans(IEnumerable<PlanCompareViewModel> model)
{
//the model passed into here is NULL
}
}
And the problem is in the controller action. As far as I am aware, it should be posting an enumerable list of PlanCompareViewModels, yet it is null. When in inspect the post data being sent, it is sending the correct params. And if I were to change 'IEnumerable' to 'FormCollection', it contains the correct values. Can anyone see why the binder is not creating the correct object? I can get around this using javascript, but that defeats the purpose! Any help would be greatly appreciated!
Your model is null because the way you're supplying the inputs to your form means the model binder has no way to distinguish between the elements. Right now, this code:
#foreach (var planVM in Model)
{
#Html.Partial("_partialView", planVM)
}
is not supplying any kind of index to those items. So it would repeatedly generate HTML output like this:
<input type="hidden" name="yourmodelprefix.PlanID" />
<input type="hidden" name="yourmodelprefix.CurrentPlan" />
<input type="checkbox" name="yourmodelprefix.ShouldCompare" />
However, as you're wanting to bind to a collection, you need your form elements to be named with an index, such as:
<input type="hidden" name="yourmodelprefix[0].PlanID" />
<input type="hidden" name="yourmodelprefix[0].CurrentPlan" />
<input type="checkbox" name="yourmodelprefix[0].ShouldCompare" />
<input type="hidden" name="yourmodelprefix[1].PlanID" />
<input type="hidden" name="yourmodelprefix[1].CurrentPlan" />
<input type="checkbox" name="yourmodelprefix[1].ShouldCompare" />
That index is what enables the model binder to associate the separate pieces of data, allowing it to construct the correct model. So here's what I'd suggest you do to fix it. Rather than looping over your collection, using a partial view, leverage the power of templates instead. Here's the steps you'd need to follow:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates).
Create a strongly-typed view in that directory with the name that matches your model. In your case that would be PlanCompareViewModel.cshtml.
Now, everything you have in your partial view wants to go in that template:
#model PlanCompareViewModel
<div>
#Html.HiddenFor(p => p.PlanID)
#Html.HiddenFor(p => p.CurrentPlan)
#Html.CheckBoxFor(p => p.ShouldCompare)
<input type="submit" value="Compare"/>
</div>
Finally, your parent view is simplified to this:
#model IEnumerable<PlanCompareViewModel>
#using (Html.BeginForm("ComparePlans", "Plans", FormMethod.Post, new { id = "compareForm" }))
{
<div>
#Html.EditorForModel()
</div>
}
DisplayTemplates and EditorTemplates are smart enough to know when they are handling collections. That means they will automatically generate the correct names, including indices, for your form elements so that you can correctly model bind to a collection.
Please read this: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
You should set indicies for your html elements "name" attributes like planCompareViewModel[0].PlanId, planCompareViewModel[1].PlanId to make binder able to parse them into IEnumerable.
Instead of #foreach (var planVM in Model) use for loop and render names with indexes.
In my case EditorTemplate did not work. How I did -
ViewModel file -
namespace Test.Models
{
public class MultipleFormViewModel
{
public int Value { get; set; }
public string Title { get; set; }
}
}
Main View (cshtml) file -
#model List<MultipleFormViewModel>
#{
var list = new List<MultipleFormViewModel>();
ViewData["index"] = 0;
}
#using (Html.BeginForm("SaveDataPoint", "MultipleForm", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
for (int i = 0; i < 3; i++)
{
var dataPoint = new MultipleFormViewModel();
list.Add(dataPoint);
ViewData["index"] = i;
#Html.Partial("_AddDataPointFormPartial", ((List<MultipleFormViewModel>)list)[i], ViewData)
}
<input type="submit" value="Save Data Points" />
}
Partial View (under Shared folder) file -
#model MultipleFormViewModel
#{
var index = ViewData["index"];
}
<div>
<div>
<label>Value:</label>
<input asp-for="Value" name="#("["+index+"].Value")" id="#("z"+index+"__Value")" />
</div>
<div>
<label>Title:</label>
<input asp-for="Title" name="#("["+index+"].Title")" id="#("z"+index+"__Title")" />
</div>
</div>
And Finally Controller -
[HttpPost]
public IActionResult SaveDataPoint(List<MultipleFormViewModel> datapoints)
{
Console.WriteLine(datapoints.Count);
//Write your code
return Content("hello..");
}

Why is my model not passed as parameter with form post

I'm having trouble understanding why my model is not passed along with its values to my controller when posting a form.
I have a view with a strongly typed model (UnitContract) that is being fetched from a webservice, that holds a set of values. In my action I'm trying to fetch int ID and bool Disabled fields that exists in my model. When debugging, I see that my model being passed from the form doesn't contain any values at all. What am I missing?
My view (UnitContract as strongly typed model):
...
<form class="pull-right" action="~/UnitDetails/EnableDisableUnit" method="POST">
<input type="submit" class="k-button" value="Enable Unit"/>
</form>
My controller action:
[HttpPost]
public ActionResult EnableDisableUnit(UnitContract model)
{
var client = new UnitServiceClient();
if (model.Disabled)
{
client.EnableUnit(model.Id);
}
else
{
client.DisableUnit(model.Id);
}
return RedirectToAction("Index", model.Id);
}
Sounds like you need to add the fields from your model to your form. Assuming your view accepts a UnitContract model, then something like this should work:
<form class="pull-right" action="~/UnitDetails/EnableDisableUnit" method="POST">
#Html.HiddenFor(x => x.Id)
#Html.HiddenFor(x => x.Disabled)
<input type="submit" class="k-button" value="Enable Unit"/>
</form>
Now when you submit the form, it should submit the fields to your model.
The MVC framework will use the data from the form to create the model. As your form is essentially empty, there is no data to create the model from, so you get an object without any data populated.
The only data that is sent from the browser in the request when you post the form, is the data that is inside the form. You have to put the data for the properties in the model as fields in the form, so that there is something to populate the model with.
Look into using #Html.HiddenFor(). Put these in your form, and the data you want to see posted back to your controller should be there. For example, your form would look something like...
<form class="pull-right" action="~/UnitDetails/EnableDisableUnit" method="POST">
#Html.HiddenFor(x => x.Id)
#Html.HiddenFor(x => x.IsDisabled)
<input type="submit" class="k-button" value="Enable Unit"/>
</form>
Let's say you have a model like this:
public class UnitContract
{
public int Id { get; set; }
public DateTime SignedOn { get; set; }
public string UnitName { get; set; }
}
Your view would look something like this:
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>UnitContract</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.SignedOn)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.SignedOn)
#Html.ValidationMessageFor(model => model.SignedOn)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.UnitName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.UnitName)
#Html.ValidationMessageFor(model => model.UnitName)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
In your controller:
[ValidateAntiForgeryToken]
[HttpPost]
public ActionResult Edit(UnitContract unitContract)
{
// do your business here .... unitContract.Id has a value at this point
return View();
}
Hope this is helpful.

Categories

Resources