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..");
}
Related
In my app I want to make model that can have dynamic list of attributes:
Provider class
using System;
using System.Collections.Generic;
namespace DynamicForms.Models
{
public class Provider
{
public String Name { get; set; }
public List<Attribute> Attributes { get; set; }
public Provider()
{
Attributes = new List<Attribute>();
}
}
}
My controller has two methods. One to add more attributes to current model and one to save Provider model. Method to add more attributes looks like this:
[HttpPost]
// Add attribute to current provider model
public IActionResult Index(Provider provider)
{
provider.Attributes.Add(new Models.Attribute());
return View(provider);
}
And my view looks like this:
#model Provider
#using (Html.BeginForm())
{
<div>
#Html.TextBoxFor(m => m.Name)
<input type="submit" value="Add attribute" formmethod="post"/>
</div>
<div>
#foreach ( var attribute in Model.Attributes)
{
<div>
#Html.TextBoxFor(a => attribute.Name)
#Html.TextBoxFor(a => attribute.Value)
</div>
}
</div>
<div>
<input type="submit" value="Save form" formaction="Provider/Save" formmethod="post"/>
</div>
}
When I press "Add attribute" button attribute is added and a row of input fields is appearing. However when i press the button one more time nothing happens. No another row is added. Model attributes count is still 1. I've been looking for solutions all over the web but couldn't find anything that fixes my problem. How can I make form fields be added dynamically to model and displayed in vieW?
Try looping over your List with an index instead of foreach, e.g.
#for (var i = 0; i < Model.Attributes.Count; i++)
{
<div>
#Html.TextBoxFor(a => Model.Attributes[i].Name)
#Html.TextBoxFor(a => Model.Attributes[i].Value)
</div>
}
You want the names to be formatted something like this Attributes[0].Name, etc.
You wrote a method for Post, but you wrote a method for get.
Get method
public IActionResult Index()
{
Provider provider = new Provider();
provider.Attributes.Add(new Models.Attribute());
return View(provider);
}
Post metod
[HttpPost]
// Add attribute to current provider model
public IActionResult Index(Provider provider)
{
provider.Attributes.Add(new Models.Attribute());
return View(provider);
}
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)
SCENARIO:
First of all, sorry for my english.
What I'm trying to do is posting trough form-POST the following object:
public class AppConfigViewModelInput
{
public string Setting { get; set; }
public string Value { get; set; }
}
to the following method:
[HttpPost]
public ActionResult Index(List<AppConfigViewModelInput> listOfAppConfigToUpdate)
{ ... }
But this input-object is constructed by only two properties of the view-object that I use to show the data on my razor page:
public class AppConfigViewModel : AppConfigViewModelInput
{
public string Description { get; set; }
public string ConfigType { get; set; }
public int ViewOrderInWebAdmin { get; set; }
public string ViewSpecialBackgroundColor { get; set; }
}
I was reading a lot of questions and blogs (check out SO References in the question). Finally I could get the following code for my razor page (I only post the form-code section):
#model List<PGWebAdmin.Models.AppConfigViewModel>
#{
var itemCnt = 0;
}
#foreach (var item in Model)
{
itemCnt++;
<input type="hidden" name="AppConfigViewModelInput.Index" value="#itemCnt" />
<input type="text" class="input-sm form-control" value="#item.Value" name="AppConfigViewModelInput[#itemCnt].Value"/>
<input type="text" name="AppConfigViewModelInput[#itemCnt].Setting" value="#item.Setting"/>
}
and the form is created by:
#using (Html.BeginForm("Index", "AppConfig",
FormMethod.Post, new { #class = "navbar-form navbar-right", role = "search" }))
{
QUESTION:
I could send the data, I'm checking with the dev tool the following information:
that is posted to the method, and the method is hit, but the value of the parameter is null:
I tested and corrected and tried several ways to do this but this is the far away I could get, and I can't understand what's happening.
I'm doing something wrong? Why I'm still getting null?
Any help will be preciated.
Thanks!
REFERENCES:
MVC post a list of complex objects
How can I post a list of items in MVC
Posting to a list<modeltype> MVC3
You need the change the name of the parameter to match what you are sending in the "name" field.
ie change your post controller:
[HttpPost]
public ActionResult Index(List<AppConfigViewModelInput> AppConfigViewModelInput)
{ ... }
Try this? It's maybe not the best answer but it should work for your purposes.
[HttpPost]
public ActionResult Index(AppConfigViewModelInput[] listOfAppConfigToUpdate)
{ ... }
And the html like this ..
#model List<PGWebAdmin.Models.AppConfigViewModel>
#{
var itemCnt = 0;
}
#foreach (var item in Model)
{
itemCnt++;
<input type="text" class="input-sm form-control" value="#item.Value" name="listOfAppConfigToUpdate[#itemCnt].Value"/>
<input type="text" name="listOfAppConfigToUpdate[#itemCnt].Setting" value="#item.Setting"/>
}
I removed the top input of index.. i don't see where it fits in. You can convert the array to a list inside your Index method.
I dont see the reason you send the AppConfigModelInput.Index, i think that might be your problem. The message you send should not contain data that is not part of the model
Your input file names are wrong ! Since your HttpPost action expects a collection of AppConfigViewModel. You don't really need the AppConfigViewModelInput. prefix for your input field names. For model binding to work, Your input field names should be like
<input type="hidden" name="[0].Index" value="" />
<input type="hidden" name="[1].Index" value=" />
Also make sure your form elements are in a form tag.
The below should work.
#model List<PGWebAdmin.Models.AppConfigViewModel>
#{
var itemCnt = 0;
}
#using (Html.BeginForm())
{
foreach (var item in Model)
{
<input type="hidden" name="[#itemCnt].Index" value="#itemCnt" />
<input type="text" value="#item.Value" name="AppConfigViewModelInput[#itemCnt].Value"/>
<input type="text" name="[#itemCnt].Setting" value="#item.Setting"/>
itemCnt++;
}
<input type="submit" value="Save" class="btn btn-default" />
}
My controller I cant seem to bind the submitdata method to check box with id=answerA. At the moment I just want to insert one value the later will adjust accommodate 3 answer options:
[HttpPost]
public ActionResult SubmitData(QuestionnareModels models)
{
using (QUESTIONNAREDataContext db = new QUESTIONNAREDataContext())
{
SubmitTable answerSubmit = new SubmitTable()
{
SubmitID = 1,
CorrectAnswer = models.AnswerA
};
db.SubmitTables.InsertOnSubmit(answerSubmit);
db.SubmitChanges();
}
return RedirectToAction("Index");
}`
My view, where I render my check boxes from a linq database:
<div class="container">
<h2><mark>Questionnare system</mark></h2>
#using (Html.BeginForm("uploadAnswer"))
{
foreach (var questionType in Model)
{
#questionType.Question
<br/>
<br/>
<input type="checkBox" id="AnswerA" />
#questionType.AnswerA
<br />
<input type="checkBox" id="AnswerB" />
#questionType.AnswerB
<br />
<input type="checkBox" id="AnswerC" />
#questionType.Answerc
<br/>
<br/>
}
<br />
<button type="submit" class="btn btn-success" id="submitDataButton">Submit</button>
}
</div>
Your code has multiple problems including invalid html (duplicate id attributes), missing name attributes (nothing in the form will post back), incorrect use of checkboxes (unchecked checkboxes do not post back so there is no way of knowing which answer belongs with which question), posting back to the wrong method and allowing multiple answers for the same question. You need to create view models that represent what you want to display and edit
View models
public class QuestionVM
{
public int ID { get; set; }
public string Text { get; set; }
public int SelectedAnswer { get; set; }
public List<AnswerVM> AnswerList { get; set; }
}
public class AnswerVM
{
public int ID { get; set; }
public string Text { get; set; }
}
Controller
public ActionResult Create()
{
List<QuestionVM> model = new QuestionVM();
// populate your questions and the possible answers for each question
return View(model);
}
[HttpPost]
public ActionResult Create(List<QuestionVM> model)
{
// The model now contains the ID of each question and the ID of its selected answer
}
View
#model List<QuestionVM>
#using(Html.BeginForm())
{
for(int i = 0; i < Model.Count; i++)
{
#Html.HiddenFor(m => m[i].ID)
#Html.DisplayFor(m => m[i].Text)
foreach(AnswerVM answer in Model[i].AnswerList)
{
string id = "answer" + answer.ID;
#Html.RadioButtonFor(m => m[i].SelectedAnswer, answer.ID, new { id = #id })
<label for="#id">#answer.Text</label>
}
}
<input type="submit" value="Save" />
}
Looking at you controller code, I suspect your database structure is wrong. You would need a table for Questions (ID, Text, etc), a table for Answers (ID, Text, QuestionID, etc) and a table for users answers to questions (say) UserQuestionaires (UserID, QuestionID, AnswerID, etc)
<div class="container">
<h2><mark>Questionnare system</mark></h2>
#using (Html.BeginForm("SubmitData"))
{
foreach (var questionType in Model)
{
#questionType.Question
<br/>
<br/>
<input type="checkBox" name="AnswerA" />
#questionType.AnswerA
<br />
<input type="checkBox" name="AnswerB" />
#questionType.AnswerB
<br />
<input type="checkBox" name="AnswerC" />
#questionType.Answerc
<br/>
<br/>
<button type="submit" class="btn btn-success" id="submitDataButton">Submit</button>
}
<br />
}
</div>
Two issue
BeginForm should have method or action you have specificed in Controller
Submit button must be inside form.
You should not use ID. You have to use name.
If i understand your problem, I can see the 'form' action in html is "uploadAnswer" where as you posted the action method code for "SubmitData".
So what happens if you change "uploadAnswer" in Html.BeginForm("uploadAnswer") to Html.BeginForm("SubmitData"), are you able to hit the controller ?
Also for proper model binding, the name of html element and the property name of model should be same. So change html to: <input type="checkBox" name="AnswerA" id="AnswerA" /> (Assuming "AnswerA" is a property of your Questionaire model.
If you want to model bind to a collection, please take a look at here and here.
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.