I am working on a "Survey" application in MVC 5. Basically it's an application to support surveys for users to fill out. The user can also go back at any time later on and change their answer too.
UI: I have a table with each row containing the actual survey questions, their answer (Yes/No/N-A, from a dropdownlistfor) and row-based comments. Their answer (dropdownlist selection) and comments for each row should be the POST vars submitted to the action.
Problem: I'm having trouble getting any post variables, which are always null. Here's some of my code...
ViewModel (SurveyResultsViewModel.cs)
[NotMapped]
public class SurveyResultsViewModel
{
// ------ hidden
[Key]
public int EventId { get; set; }
[Display(Name = "Category")]
public string SurveyCategory { get; set; }
[Display(Name = "Question")]
public string SurveyQuestionText { get; set; }
public int SurveyQuestionId { get; set; }
public SelectList SurveyAnswerOption { get; set; }
public int SurveyResultsId { get; set; }
[Display(Name = "Answer")]
public string SelectedSurveyAnswer { get; set; }
public string Comments { get; set; }
}
Controller GET...
public ActionResult Index(int id)
{
:
:
:
List<SurveyResultsViewModel> vm = new List<SurveyResultsViewModel>();
var Surveyresults = db.SurveyResults
.Where(i => i.EventId == id)
.Include(a => a.SurveyQuestion)
.OrderBy(a=>a.SurveyQuestion.SurveyQuestionCategory.Id)
.Include(a => a.Event);
foreach (SurveyResult ar in Surveyresults)
{
var newVmRecord = new SurveyResultsViewModel();
newVmRecord.EventId = id;
newVmRecord.SurveyAnswerOption = new SelectList(db.SurveyAnswerOptions, "AnswerOptionText", "AnswerOptionText", ar.SurveyAnswer);
newVmRecord.SurveyCategory = ar.SurveyQuestion.SurveyQuestionCategory.SurveyQuestionCategory1;
newVmRecord.SurveyQuestionId = ar.SurveyQuestionId.Value;
newVmRecord.SurveyQuestionText = ar.SurveyQuestion.SurveyQuestionText;
newVmRecord.Comments = ar.Comments;
newVmRecord.SelectedSurveyAnswer = ar.SurveyAnswer;
newVmRecord.SurveyResultsId = ar.Id;
vm.Add(newVmRecord);
}
return View(vm.ToList());
}
Here's part of my View....
#model IEnumerable<SurveyApp.ViewModels.SurveyResultsViewModel>
:
:
<div class="ibox-content">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<table class="table table-striped">
<tr>
<th class="col-md-2">
#Html.DisplayNameFor(model => model.SurveyCategory)
</th>
<th class="col-md-5">
#Html.DisplayNameFor(model => model.SurveyQuestionText)
</th>
<th>
#Html.DisplayNameFor(model => model.SelectedSurveyAnswer)
</th>
<th>
#Html.DisplayNameFor(model => model.Comments)
</th>
</tr>
#{
int i = 0;
}
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.SurveyCategory)
</td>
<td>
#Html.DisplayFor(modelItem => item.SurveyQuestionText)
</td>
<td>
#Html.TextBoxFor(modelItem => item.SelectedSurveyAnswer)
#Html.DropDownListFor(modelItem => item.SelectedSurveyAnswer, item.SurveyAnswerOption, "--Select--")
</td>
<td>
#Html.HiddenFor(modelItem => item.SurveyResultsId)
#Html.TextAreaFor(modelItem => item.Comments)
</td>
</tr>
i++;
}
</table>
<input type="submit" value="Save" class="btn btn-primary"/>
#Html.ActionLink("Cancel", "Index", null, new {#class = "btn btn-white"})
}
</div>
The view appears to be displaying correctly. But, when I view source, I noticed that, when rendered, unique NAME values are not given for the "post"-related controls. I'm thinking that this may be contributing to the issue? For example, here's the rendering for one of the dropdownlistfor's....
<select id="item_SelectedSurveyAnswer" name="item.SelectedSurveyAnswer">
<option value="">--Select--</option>
<option selected="selected" value="Yes">Yes</option>
<option value="No">No</option>
<option value="N/A">N/A</option>
</select>
Here's my preliminary POST method. The var mySurveyResult is always NULL...
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(
//[Bind(Include = "SurveyResultsId,SelectedSurveyAnswer,Comments")]
List<SurveyResultsViewModel> mySurveyResult)
{
if (ModelState.IsValid)
{
for (int i = 0; i < mySurveyResult.Count(); i++)
{
Console.WriteLine(mySurveyResult[i].SelectedSurveyAnswer);
}
db.Entry(mySurveyResult).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View();
}
As you can see I haven't fully written this method yet. But, essentially, this post action will need to loop through each answer posted and save it to the db. I haven't gotten that far yet because I keep getting null for the post parameter.
Again, I'm thinking that the problem may be that there are no unique name attributes maybe? Maybe this is affecting the POST? Any feedback would be appreciated. Also, if you see (from my posted code examples) that I'm not doing things correctly please let me know.
Please let me thank you in advance!
You need to use for rather than foreach to iterate over your items:
#for (var i = 0; i < Model.Count(); i++)
{
<tr>
<td>
#Html.DisplayFor(modelItem => Model[i].SurveyCategory)
</td>
<td>
#Html.DisplayFor(modelItem => Model[i].SurveyQuestionText)
</td>
<td>
#Html.TextBoxFor(modelItem => Model[i].SelectedSurveyAnswer)
#Html.DropDownListFor(modelItem => Model[i].SelectedSurveyAnswer, Model[i].SurveyAnswerOption, "--Select--")
</td>
<td>
#Html.HiddenFor(modelItem => Model[i].SurveyResultsId)
#Html.TextAreaFor(modelItem => Model[i].Comments)
</td>
</tr>
}
This will give your fields names like [0].SelectedSurveyAnswer, instead of just SelectedSurveyAnswer, allowing the modelbinder to actually be able to bind the posted values back to a list.
Related
I am trying to post model which contains List<> item looks like below:
public class AddSubscriptionPlanModel
{
public AddSubscriptionPlanModel()
{
AllFeatures = new List<Feature>();
}
public int Id { get; set; }
public int SubscriptionPlanId { get; set; }
public int SubscriptionId { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
[Display(Name = "No Of Users")]
public int? NoOfUsers { get; set; }
[Display(Name = "Duration Type")]
public DurationType DurationType { get; set; }
public int Duration { get; set; }
public decimal Amount { get; set; }
public int SubscriptionPlanTrailId { get; set; }
public int FeatureId { get; set; }
public DateTime CreatedDateUtc { get; set; }
public DateTime LastUpdatedUtc { get; set; }
public int CreatedBy { get; set; }
public int LastUpdatedBy { get; set; }
public List<Feature> AllFeatures { get; set; }
}
I am populating all required fields on get method and feeding List from database
public ActionResult Mapping(int id)
{
var features = FeatureManager.GetAllFeatures();
var model = new Ejyle.DevAccelerate.Web.App.Models.SubscriptionPlan.AddSubscriptionPlanModel();
model.AllFeatures = features;
model.Id = id;
model.SubscriptionId = id;
model.NoOfUsers = 20;
model.SubscriptionPlanId = 1;
model.SubscriptionPlanTrailId = 1;
model.Amount = 1000;
model.CreatedBy = 1;
model.LastUpdatedBy = 1;
model.FeatureId = 1;
model.Duration = 20;
return View(model);
}
And my view code looks like below :
#using (Html.BeginForm("Mapping", "SubscriptionPlans", FormMethod.Post))
{
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.Amount)
#Html.HiddenFor(model => model.Duration)
#Html.HiddenFor(model => model.FeatureId)
#Html.HiddenFor(model => model.CreatedBy)
#Html.HiddenFor(model => model.LastUpdatedBy)
#Html.HiddenFor(model => model.NoOfUsers)
#Html.HiddenFor(model => model.SubscriptionId)
#Html.HiddenFor(model => model.SubscriptionPlanId)
#Html.HiddenFor(model => model.SubscriptionPlanTrailId)
#Html.HiddenFor(model => model.AllFeatures)
<table class="table table-striped">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.IsActive)
</th>
</tr>
</thead>
<tbody>
#for (int i=0; i < Model.AllFeatures.Count; i++)
{
<tr>
#Html.HiddenFor(model => model.AllFeatures[i].Id)
#Html.HiddenFor(model => model.AllFeatures[i].Name)
#Html.HiddenFor(model => model.AllFeatures[i].IsActive)
<td>
#Html.TextBoxFor(model => model.AllFeatures[i].Id)
</td>
<td>
#Html.TextBoxFor(model => model.AllFeatures[i].Name)
</td>
<td>
#Html.EditorFor(model => model.AllFeatures[i].IsActive)
</td>
</tr>
}
</tbody>
</table>
<input type="submit" class="btn btn-success" value="Submit" name="Submit"/>
}
On View, the list items rendered properly and even I am able to edit those fields.
But On post, all properties have the value except AllFeatures list item property which always shows count = 0.
Edit 1: This is how my view looks like while rendering features
Edit 2:
Signature of the method posting to:
[HttpPost]
public ActionResult Mapping(AddSubscriptionPlanModel model)
{
}
But On post, all properties have the value except AllFeatures list item property which always shows count = 0.
AllFeatures is a generic list of Feature. When you do this:
#Html.HiddenFor(model => model.AllFeatures)
The Razor view engine will render this:
<input id="AllFeatures"
name="AllFeatures"
type="hidden"
value="System.Collections.Generic.List`1[Namespace.Feature]">
<!--Where Namespace is the Namespace where Feature is defined. -->
In other words, HiddenFor calls the ToString() on the item and simply puts that into the input tag's value attribute.
What happens upon POST
When you post the form, the DefaultModelBinder is looking for a property named AllFeatures but of type string so it can assign this to it:
System.Collections.Generic.List`1[Namespace.Feature]
It does not find one since your model does not have AllFeatures of type string, so it simply ignores it and binds all the other properties.
AllFeatures list item property which always shows count = 0.
Yes, it will and this is not the AllFeatures list which you posted but the one from the constructor which clearly is an empty list with a count 0:
public AddSubscriptionPlanModel()
{
AllFeatures = new List<Feature>();
}
I am not sure why you are sending all the features to the client (browser) and then you need to post it back to the server.
Solution
To fix the issue, simply remove this line:
#Html.HiddenFor(model => model.AllFeatures)
Now it will not cause any confusion during binding and MVC will bind the items in the loop to the AllFeatures property.
In fact, the only code you really need, as far as I can tell from your question is this(I could be wrong if you need the hidden fields for some other reason. But if you just want the user to edit the AllFeatures, you do not need any of the hidden fields):
#for (int i = 0; i < Model.AllFeatures.Count; i++)
{
<tr>
<td>
#Html.TextBoxFor(model => model.AllFeatures[i].Id)
</td>
<td>
#Html.TextBoxFor(model => model.AllFeatures[i].Name)
</td>
<td>
#Html.EditorFor(model => model.AllFeatures[i].IsActive)
</td>
</tr>
}
I think your issue might be the nested property. You may want to review the posted form and see what it looks like.
Your markup looks correct assuming that the model binding would work for a nested property - which I am not sure it does. (Just to make sure I searched for and found this old Haack post that indicates you're doing it right: Haack Post)
As a workaround (and POC), you can try the following: Add a new parameter to your action for your features, post it like your doing now but not nested (so you can't use the Model For HTML helpers) and then in the controller, you can set it back to your main object's features property.
This how to do it, as in the context of binding to a list from a view is incorrect.
View:
#model Testy20161006.Controllers.AddSubscriptionPlanModel
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Mapping</title>
</head>
<body>
#*I am using controller home*#
#using (Html.BeginForm("Mapping", "Home", FormMethod.Post))
{
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.Amount)
#Html.HiddenFor(model => model.Duration)
#Html.HiddenFor(model => model.FeatureId)
#Html.HiddenFor(model => model.CreatedBy)
#Html.HiddenFor(model => model.LastUpdatedBy)
#Html.HiddenFor(model => model.NoOfUsers)
#Html.HiddenFor(model => model.SubscriptionId)
#Html.HiddenFor(model => model.SubscriptionPlanId)
#Html.HiddenFor(model => model.SubscriptionPlanTrailId)
#Html.HiddenFor(model => model.AllFeatures)
<table class="table table-striped">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.IsActive)
</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.AllFeatures.Count; i++)
{
<tr>
#*BE SURE to get rid of these*#
#*#Html.HiddenFor(model => model.AllFeatures[i].Id)
#Html.HiddenFor(model => model.AllFeatures[i].Name)
#Html.HiddenFor(model => model.AllFeatures[i].IsActive)*#
<td>
#Html.TextBoxFor(model => model.AllFeatures[i].Id)
</td>
<td>
#Html.TextBoxFor(model => model.AllFeatures[i].Name)
</td>
<td>
#Html.EditorFor(model => model.AllFeatures[i].IsActive)
</td>
</tr>
}
</tbody>
</table>
<input type="submit" class="btn btn-success" value="Submit" name="Submit" />
}
</body>
</html>
Controller:
[HttpPost]
public ActionResult Mapping(AddSubscriptionPlanModel addSubscriptionModel, FormCollection formCollection)
{
var AllFeatures = String.Empty;
var index = "AllFeatures[";
foreach (var item in formCollection)
{
if (item.ToString().Contains(index))
{
AllFeatures += " " + formCollection.GetValues(item.ToString())[0];
}
}
//put breakpoint here to see userValues
I'm struggling with (perhaps) a simple challenge.
I have the following classes:
public class Product
{
public int ID { get; set; }
[Display(Name ="Product Name")]
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int? CategoryID { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public int ID { get; set; }
[Display(Name = "Category Name")]
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
To store the data I'm using EF Core
public class StoreContext : DbContext
{
public StoreContext(DbContextOptions<StoreContext> options) : base(options) { }
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
}
And in my ProductsController I have the following GET method:
public async Task<IActionResult> Index(int category, string search)
{
var storeContext = _context.Products.Include(p => p.Category) as IQueryable<Product>;
if (category != 0)
{
storeContext = storeContext.Where(p => p.Category.ID == category);
}
if (search != null)
{
storeContext = storeContext.Where(p => p.Category.Name == search);
}
var categories = storeContext.OrderBy(p => p.Category.Name).Select(p => p.Category.Name).Distinct();
if (!String.IsNullOrEmpty(search))
{
storeContext = storeContext.Where(p => p.Name.Contains(search)||
p.Description.Contains(search) ||
p.Category.Name.Contains(search));
ViewBag.Search = search;
}
ViewBag.Category = new SelectList(categories);
return View(await storeContext.ToListAsync());
}
My Index.cshtml looks like as follows:
#model IEnumerable<BabyStore.Models.Product>
#{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" asp-controller="Products" method="get">
<div class="form-group">
<table>
<tr>
<td>
<label>Filtered by Category: </label>
</td>
<td>
<select class="form-control" asp-items="ViewBag.Category">
<option value="">-- Select Category --</option>
</select>
</td>
<td>
<input type="submit" value="Filter" id="search"/>
<input type="hidden" name="search" id="search"/>
</td>
</tr>
</table>
</div>
</form>
<br />
<table class="table">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.Description)
</th>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.Category.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.Description)
</td>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Category.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="#item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="#item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="#item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Pushing the "Filter" button should cause refreshing the View and filtering by Category.Name
But, unfortunately it does not work.
In my browser I read
http://localhost:53827/Products?search=
So no parameter has been passed to search.
If I enter by hand
http://localhost:53827/Products?search=clothes
it works perfectly
In the Startup.cs I have the following Route
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Any idea how to solve it?
I am new to ASP.net MVC and I have looked at other questions and tried multiple methods to populate a view with 2 IEnumerables using a ViewModel. It seems using the groupby methods in the controller and passing them to the View has created some problems.
The overall problem is that the view doesn't accept ListCatViewModel. It gives the error: IEnumerable does not contain a definition for 'Category' and no extension method 'Category' accepting a first argument of type 'IEnumerable
In the multiple questions that I have seen on Stackoverflow and other sources we are supposed to combine the two lists with a variable and then pass that variable to the View. After that we are supposed to reference the ViewModel. When I reference the ViewModel I cannot access the properties in each IEnumerable (NotesList and RelList). I am looking for direction on how to change the controller and/or reference in the View to accept the 2 separate collections to ultimately create two tables.
Summary of Code: CatViewModel and RelViewModel hold different properties. ListCatViewModel contains the IEnumerable collections.
public class CatViewModel
{
public string Category { get; set; }
public decimal CountOfLoans { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:c}")]
public decimal TotalVolume { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:c}")]
public decimal UnfundedCommitment { get; set; }
}
public class RelViewModel
{
public string RelationshipName { get; set; }
public decimal CountOfLoans { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:c}")]
public decimal TotalVolume { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:c}")]
public decimal UnfundedCommitment { get; set; }
}
public class ListCatViewModel
{
public IEnumerable<CatViewModel> NotesList { get; set; }
public IEnumerable<RelViewModel> RelList { get; set; }
}
Controller:
public ActionResult Index()
{
ApplicationDbContext db = new ApplicationDbContext();
ListCatViewModel LCVM = new ListCatViewModel();
LCVM.NotesList = GetCatList();
LCVM.RelList = GetRelList();
return View(LCVM);
}
public IEnumerable<CatViewModel> GetCatList() //Category group by query
{
ApplicationDbContext db = new ApplicationDbContext();
IEnumerable<CatViewModel> CatVM = db.LoanPortfolio.GroupBy(i => i.Category)
.Select(g => new CatViewModel
{
Category = g.Key,
CountOfLoans = g.Count(),
TotalVolume = g.Sum(i => i.Volume),
UnfundedCommitment = g.Sum(i => i.Unfunded),
//Average = g.Average(i => i.Volume)
})
.OrderBy(c => c.Category)
.AsEnumerable();
return CatVM;
}
public IEnumerable<RelViewModel> GetRelList()
{
ApplicationDbContext db = new ApplicationDbContext();
IEnumerable<RelViewModel> RelVM = db.LoanPortfolio.GroupBy(i => i.RelationshipName)
.Select(g => new RelViewModel
{
RelationshipName = g.Key,
CountOfLoans = g.Count(),
TotalVolume = g.Sum(i => i.Volume),
UnfundedCommitment = g.Sum(i => i.Unfunded),
//Average = g.Average(i => i.Volume)
})
.OrderBy(c => c.RelationshipName)
.AsEnumerable();
return RelVM;
}
View:
View:
#model IEnumerable<Risk.ViewModel.ListCatViewModel>
#{
ViewBag.Title = "GroupByLoanPAllowanceB";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Category</h2>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.NotesList.Category)
</th>
<th>
#Html.DisplayNameFor(model => model.NotesList.CountOfLoans)
</th>
<th>
#Html.DisplayNameFor(model => model.NotesList.TotalVolume)
</th>
<th>
#Html.DisplayNameFor(model => model.NotesList.UnfundedCommitment)
</th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.NotesList.Category)
</td>
<td>
#Html.DisplayFor(modelItem => item.CountOfLoans)
</td>
<td>
#Html.DisplayFor(modelItem => item.TotalVolume)
</td>
<td>
#Html.DisplayFor(modelItem => item.UnfundedCommitment)
</td>
</tr>
}
</table>
Your view is not IEnumerable<Risk.ViewModel.ListCatViewModel>, it is now ListCatViewModel
Try:
#model Risk.ViewModel.ListCatViewModel
#{
ViewBag.Title = "GroupByLoanPAllowanceB";
Layout = "~/Views/Shared/_Layout.cshtml";
}
And when iterating over the list, you need to access it off the Model
#foreach (var item in Model.NotesList)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Category)
</td>
<td>
#Html.DisplayFor(modelItem => item.CountOfLoans)
</td>
<td>
#Html.DisplayFor(modelItem => item.TotalVolume)
</td>
<td>
#Html.DisplayFor(modelItem => item.UnfundedCommitment)
</td>
</tr>
}
Assuming you're just trying to display the data, create some templates in the Views -> Shared -> DisplayTemplates folder (you may need to create it), and then use DisplayFor. Here's an example for your CatViewModel:
CatViewModel.cshtml
#model Your.Fully.Qualified.Namespace.CatViewModel
<tr>
<td>
#Model.Category
</td>
<td>
#Model.CountOfLoans
</td>
<td>
#Model.TotalVolume
</td>
<td>
#Model.UnfundedCommitment
</td>
</tr>
Parent view:
#model Risk.ViewModel.ListCatViewModel
#{
ViewBag.Title = "GroupByLoanPAllowanceB";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Category</h2>
<table class="table">
<tr>
<th>
Category
</th>
<th>
Count Of Loans
</th>
<th>
Total Volume
</th>
<th>
Unfunded Commitment
</th>
</tr>
#Html.DisplayFor(m => m.NotesList)
</table>
The Razor engine is smart enough to look for a partial view with the same name as the model (in the aforementioned folder) when you call either EditorFor or DisplayFor. So, if the property was of type Foo, and you had a view called Foo.cshtml, it would be used for that property. If you supply a collection, it will also iterate through the collection and generate markup for each item.
There's also an overload that allows you to specify the template to use, if you don't want to follow the convention I mentioned above.
I have a problem with the action that recives a complex ViewModel by POST and all of its object components are null, even though I have initialized them in Action and have returned the whole ViewModel to the View using GET method.
Let me explain the situation. I have a complex model for a View that consists of three sections: Applicant details, Application details, and a list of Recordings. This View is complex to (1) let me see the details of Applicant I am creating application for, (2) have a list of recordings I would like to choose from which then I can add to Application. This is my ViewModel:
public class ApplicantApplicationRecordingsViewModel
{
// Applicant
public Applicant Applicant { get; set; }
// Application
public Application Application { get; set; }
public SelectList UsageTypeSelectList { get; private set; }
public SelectList UsageEndAppSelectList { get; private set; }
// Recordings
public IEnumerable<RecordingViewModelApp>
RecordingsViewModelApp { get; set; }
public ApplicantApplicationRecordingsViewModel()
: this(new MyDBContext())
{
}
public ApplicantApplicationRecordingsViewModel(MyDBContext dbContext)
{
PopulateUsageTypeSelectList(dbContext);
PupulateUsageEndAppSelectList(dbContext);
}
private void PopulateUsageTypeSelectList(MyDBContext dbContext,
int? usageTypeSelected = null)
{
IEnumerable<UsageType> utQuery =
dbContext.UsageTypes.OrderBy(
ut => ut.UsageTypeName).ToList();
this.UsageTypeSelectList =
new SelectList(utQuery,
"UsageTypeID",
"UsageTypeName",
usageTypeSelected);
}
private void PupulateUsageEndAppSelectList(
MyDBContext dbContext,
int? usageEndAppSelected = null)
{
IEnumerable<UsageEndApp> ueaQuery =
dbContext.UsageEndApps.OrderBy(uea => uea.UsageEndAppName).ToList();
this.UsageEndAppSelectList =
new SelectList(ueaQuery,
"UsageEndAppID",
"UsageEndAppName",
usageEndAppSelected);
}
}
In the controller I simply populate a list of recordings for RecordingViewModelApp, put details of an applicant to Applicant and leave the Application object empty to be filled in a View.
public ActionResult Create(int? ApplicantID)
{
if (ApplicantID == null)
{
// Error 400. Bad Request Exception
}
ApplicantApplicationRecordingsViewModel viewModel = null;
using (MyDBContext dbContext = new MyDBContext())
{
Applicant applicant =
dbContext.Applicants.Find(ApplicantID);
if (applicant == null)
{
// Error 404. Http not found
}
List<RecordingViewModelApp> recordings =
getViewModel(
dbContext.Recordings.ToList(),
dbContext);
viewModel =
new ApplicantApplicationRecordingsViewModel(dbContext);
viewModel.Applicant = applicant;
viewModel.RecordingsViewModelApp = recordings;
}
return View(viewModel);
}
The problem is that when I return the ViewModel (ApplicantApplicationRecordingsViewModel) back to the [HttpPost] Create() Action, all the View Model's components are null, e.g. the list of RecordingViewModelApp is null. What Am I missing? I would need to understand what's going on behind the scene and why default model binding doesn't work.
[HttpPost]
[ActionName("Create")]
public ActionResult Create_post(
ApplicantApplicationRecordingsViewModel viewModelToValidate)
{
// Validation against Application only and TryToUpdate() etc.
}
CHeers!
EDIT:
The View
#model Project.ApplicantApplicationRecordingsViewModel
#{
string applicantDetails = string.Format("{0} {1} {2}",
Model.Applicant.title, Model.Applicant.firstName, Model.Applicant.lastName);
ViewBag.Title = "Create a new application for " + applicantDetails;
}
<h2>#ViewBag.Title</h2>
<hr />
#using (Html.BeginForm())
{
<h3>Details of the applicant</h3>
#Html.HiddenFor(item => Model.Applicant.ApplicantID)
#Html.HiddenFor(item => Model.Application.ApplicationID)
<table>
<tr>
<th>#Html.DisplayNameFor(item => Model.Applicant.title)</th>
<th>#Html.DisplayNameFor(item => Model.Applicant.firstName)</th>
<th>#Html.DisplayNameFor(item => Model.Applicant.lastName)</th>
<th>#Html.DisplayNameFor(item => Model.Applicant.telephone)</th>
<th>#Html.DisplayNameFor(item => Model.Applicant.mobile)</th>
<th>#Html.DisplayNameFor(item => Model.Applicant.email)</th>
</tr>
<tr>
<td class="display-field">#Html.DisplayFor(item => Model.Applicant.title)</td>
<td class="display-field">#Html.DisplayFor(item => Model.Applicant.firstName)</td>
<td class="display-field">#Html.DisplayFor(item => Model.Applicant.lastName)</td>
<td class="display-field">#Html.DisplayFor(item => Model.Applicant.telephone)</td>
<td class="display-field">#Html.DisplayFor(item => Model.Applicant.mobile)</td>
<td class="display-field">#Html.DisplayFor(item => Model.Applicant.email)</td>
</tr>
</table>
<hr /> // ----------------------------------------------------------------------------------------------
<h3>Details of the application</h3>
<table id="main">
<tr>
<td>
<table>
<tr>
<td class="editor-label first-label">#Html.DisplayNameFor(item => Model.Application.ApplicationNo)</td>
<td class="editor-field">
#Html.EditorFor(item => Model.Application.ApplicationNo)
#Html.ValidationMessageFor(item => Model.Application.ApplicationNo)
</td>
</tr>
<tr>
<td class="editor-label first-label">#Html.DisplayNameFor(item => Model.Application.StartDate)</td>
<td class="editor-field">
#Html.EditorFor(item => Model.Application.StartDate)
#Html.ValidationMessageFor(item => Model.Application.StartDate)
</td>
</tr>
<tr>
<td class="editor-label first-label">#Html.DisplayNameFor(item => Model.Application.EndDate)</td>
<td class="editor-field">
#Html.EditorFor(item => Model.Application.EndDate)
#Html.ValidationMessageFor(item => Model.Application.EndDate)
</td>
</tr>
<tr>
<td class="editor-label first-label">#Html.DisplayNameFor(item => Model.Application.UsageTypeID)</td>
<td class="editor-field">
#Html.DropDownListFor(item => Model.Application.UsageTypeID, Model.UsageTypeSelectList, "-- Select Usage --")
#Html.ValidationMessageFor(item => Model.Application.UsageTypeID)
</td>
</tr>
<tr>
<td class="editor-label first-label">#Html.DisplayNameFor(item => Model.Application.UsageEndAppID)</td>
<td class="editor-field">
#Html.DropDownListFor(item => Model.Application.UsageEndAppID, Model.UsageEndAppSelectList, "-- Select Type --")
#Html.ValidationMessageFor(item => Model.Application.UsageEndAppID)
</td>
</tr>
<tr>
<td class="editor-label first-label">#Html.DisplayNameFor(item => Model.Application.linkToPaperVer)</td>
<td class="editor-field">
#Html.EditorFor(item => Model.Application.linkToPaperVer)
#Html.ValidationMessageFor(item => Model.Application.linkToPaperVer)
</td>
</tr>
</table>
</td>
<td class="editor-label">
#Html.DisplayNameFor(item => Model.Application.Info)
</td>
<td class="editor-field">
#Html.EditorFor(item => Model.Application.Info)
#Html.ValidationMessageFor(item => Model.Application.Info)
</td>
</tr>
</table>
<hr /> // ----------------------------------------------------------------------------------------------
<h3>List of recordings</h3>
Html.RenderPartial("~/Views/Recordings/_List_App.cshtml", Model.RecordingsViewModelApp);
<hr /> // ----------------------------------------------------------------------------------------------
<p>
<input type="submit" value="Create" />
</p>
}
<div>
#Html.ActionLink("Back to List", "Index", "Applicants")
</div>
EDIT 2
PartialView:
#model IEnumerable<Project.ViewModels.RecordingViewModelApp>
#if (Model != null)
{
<div>
<table class="data-in-table">
<tr>
<th>#Html.DisplayNameFor(model => model.IsSelected)</th>
<th>#Html.DisplayNameFor(model => model.FileLocation)</th>
<th>#Html.DisplayNameFor(model => model.EnteredDate)</th>
<th>#Html.DisplayNameFor(model => model.Duration)</th>
<th>#Html.DisplayNameFor(model => model.Status)</th>
</tr>
#foreach (var item in Model)
{
<tr>
<td class="display-field">#Html.EditorFor(model => item.IsSelected)</td>
<td class="display-field">#Html.DisplayFor(model => item.FileLocation)</td>
<td class="display-field">#Html.DisplayFor(model => item.EnteredDate)</td>
<td class="display-field">#Html.DisplayFor(model => item.Duration)</td>
<td class="display-field">#Html.DisplayFor(model => item.Status)</td>
</tr>
}
</table>
</div>
}
else
{
<h3>No recordings attached to this Patient</h3>
}
EDIT 3
The RecordingViewModelApp:
public class RecordingViewModel
{
public int RecordingID { get; set; }
public string FileLocation { get; set; }
public DateTime EnteredDate { get; set; }
public int Duration { get; set; }
public string Status { get; set; }
}
public class RecordingViewModelApp : RecordingViewModel
{
public bool IsSelected { get; set; }
}
First to fix the view model. A view model should only contain simple properties representing what you want to display and/or edit
View model
public class ApplicantApplicationRecordingsViewModel
{
public Applicant Applicant { get; set; }
public Application Application { get; set; }
public IEnumerable<RecordingViewModelApp> Recordings { get; set; }
public string Title { get; set; }
public SelectList UsageTypeSelectList { get; private set; }
public SelectList UsageEndAppSelectList { get; private set; }
}
Controller (note validation checks omitted)
public ActionResult Create(int ApplicantID) // assume you must have a custom route for this?
{
ApplicantApplicationRecordingsViewModel viewModel = new ApplicantApplicationRecordingsViewModel();
Applicant applicant = dbContext.Applicants.Find(ApplicantID);
viewModel.Applicant = applicant;
viewModel.Title = string.Format("Create a new application for {0} {1} {2}", applicant.title, applicant.firstName, applicant.lastName);
viewModel.Recordings = getViewModel(dbContext.Recordings.ToList(), dbContext); // not sure what this is?
viewModel.UsageTypeSelectList = new SelectList(dbContext.UsageTypes.OrderBy(ut => ut.UsageTypeName), "UsageTypeID", "UsageTypeName");
viewModel.UsageEndAppSelectList = new SelectList(dbContext.UsageEndApps.OrderBy(uea => uea.UsageEndAppName), "UsageEndAppID", "UsageEndAppName");
return View(viewModel);
}
View
#model Project.ApplicantApplicationRecordingsViewModel
<h2>#Model.Title</h2>
#using (Html.BeginForm())
{
#Html.HiddenFor(item => Model.Applicant.ApplicantID) // include for post back but Application.ApplicationID not necessary (its a new application!)
<h3>Details of the applicant</h3>
// Add display detail for applicant, but use css for layout (position, floats etc), not tables (which are for tabular data)
<h3>Details of the application</h3>
// Add controls for Application but use LabelFor() so the label is associated with the control (otherwise its not a label)
#Html.DisplayNameFor(m => m.Application.ApplicationNo)
#Html.EditorFor(m => m.Application.ApplicationNo)
#Html.ValidationMessageFor(m => m.Application.ApplicationNo)
....
<h3>List of recordings</h3>
<table>
<thead>
.... // add table headings
</thead>
<tbody>
#Html.EditorFor(m => m.Recordings) // This uses a custom editor template to display and select recordings
</tbody>
</table>
<input type="submit" value="Create" />
}
EditorTemplate (/Views/Shared/EditorTemplates/RecordingViewModelApp.cshtml)
Note you must use either a for loop or a custom EditorTemplate to render collections. The foreach loop you used just renders duplicate id (invalid html) and name attributes without the correct indexers so will not post back to a collection.
#model RecordingViewModelApp
<tr>
<td class="display-field">
#Html.CheckBoxFor(m => m.IsSelected) // required for postback
#Html.HiddenFor(m => m.RecordingID) // required for postback
</td>
<td class="display-field">#Html.DisplayFor(m => m.FileLocation)</td>
.... // other display properties
</tr>
POST method
[HttpPost]
public ActionResult Create(ApplicantApplicationRecordingsViewModel model)
{
// model is now bound with the Applicant ID, all the properties of Application
// and the collection of Recordings with their ID and IsSelected property.
}
My Model:
public class SendFileDeviceViewModel
{
public SendFileDeviceViewModel()
{
PolicyList = new List<SendFileDevicePoliciesViewModel>();
}
public string DeviceName { get; set; }
public int DeviceId { get; set; }
public string ManagementGroupName { get; set; }
public int ManagementGroupId { get; set; }
public bool ReloadConfiguration { get; set; }
public bool ImmediateSend { get; set; }
public DateTime TimeToSend { get; set; }
public List<SendFileDevicePoliciesViewModel> PolicyList { get; set; }
}
public class SendFileDevicePoliciesViewModel
{
public int PackageTemplateId { get; set; }
public string PolicyName { get; set; }
public string PolicyType { get; set; }
public string DefinedAt { get; set; }
public bool ResendPolicy { get; set; }
}
My View:
<h2>Send files to a Device #Model.DeviceName</h2>
<h3>Reload configuration settings</h3>
#Html.CheckBoxFor(m => m.ReloadConfiguration) #Html.LabelFor(m => m.ReloadConfiguration)
<h3>Select the policies to reload</h3>
#using (Html.BeginForm())
{
#Html.HiddenFor(m => m.DeviceId)
#Html.HiddenFor(m => m.ManagementGroupId)
#Html.ValidationSummary(true)
if (Model.PolicyList.Count() > 0)
{
<table>
<caption>
Policies available for this device</caption>
<thead>
<tr>
<th scope="col">
</th>
<th scope="col">
Policy Name
</th>
<th scope="col">
Policy Type
</th>
<th scope="col">
Defined At
</th>
</tr>
</thead>
<tbody>
#foreach (var policies in Model.PolicyList)
{
<tr>
#*<td>#Html.CheckBox("PackageTemplateId", new { value = policies.PackageTemplateId })</td>*#
<td>#Html.CheckBoxFor(m => policies.ResendPolicy)</td>
<td>#policies.PolicyName</td>
<td>#policies.PolicyType</td>
<td>#policies.DefinedAt</td>
</tr>
}
</tbody>
</table>
}
<div class="editor-label">
#Html.LabelFor(m => m.ImmediateSend)
</div>
<div class="editor-field">
#Html.CheckBoxFor(m => m.ImmediateSend)
</div>
<div class="editor-label">
#Html.LabelFor(m => m.TimeToSend)
</div>
<div class="editor-field">
#Html.EditorFor(m => m.TimeToSend)
</div>
<p>
<input type="submit" value="Send files" /></p>
My issue is when retrieving model from controller the PolicyList is always empty. Am I missing somethign simple here?
Two problems:
Your first problem is that you are resetting your list in your constructor, so when the form is posted and the model binder instantiates an instance of your model, you're re-setting the list. Change it to do a coalesce to only re-assign if the list is null:
public SendFileDeviceViewModel()
{
PolicyList = PolicyList ?? new List<SendFileDevicePoliciesViewModel>();
}
Your next problem is your foreach. In order to index the name attribute correctly (so the model binder can do it's stuff), you need to use a for loop. Also, keep the Id in a HiddenFor.
Try this in place of your foreach:
#for (int i = 0; i < Model.PolicyList.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.PolicyList[i].PackageTemplateId)
#Html.CheckBoxFor(m => m.PolicyList[i].ResendPolicy)
</td>
<td>#Model.PolicyList[i].PolicyName</td>
<td>#Model.PolicyList[i].PolicyType</td>
<td>#Model.PolicyList[i].DefinedAt</td>
</tr>
}
The reason for this is because you didn't respect the naming convention of your input fields. You should replace the foreach loop in the view with a for loop or a custom editor template:
<tbody>
#for (var i = 0; i < Model.PolicyList.Count; i++)
{
<tr>
<td>#Html.CheckBoxFor(x => x.PolicyList[i].ResendPolicy)</td>
<td>#Html.DisplayFor(x => x.PolicyList[i].PolicyName)</td>
<td>#Html.DisplayFor(x => x.PolicyList[i].PolicyType)</td>
<td>#Html.DisplayFor(x => x.PolicyList[i].DefinedAt)</td>
</tr>
}
</tbody>
Also now only the ResendPolicy property will be bound because that's the only one that has a corresponding input field (a checkbox in your case). If you want to bind the others as well you might need to include corresponding hidden fields:
<tbody>
#for (var i = 0; i < Model.PolicyList.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(x => x.PolicyList[i].PackageTemplateId)
#Html.CheckBoxFor(x => x.PolicyList[i].ResendPolicy)
</td>
<td>
#Html.HiddenFor(x => x.PolicyList[i].PolicyName)
#Html.DisplayFor(x => x.PolicyList[i].PolicyName)
</td>
<td>
#Html.HiddenFor(x => x.PolicyList[i].PolicyType)
#Html.DisplayFor(x => x.PolicyList[i].PolicyType)
</td>
<td>
#Html.HiddenFor(x => x.PolicyList[i].DefinedAt)
#Html.DisplayFor(x => x.PolicyList[i].DefinedAt)
</td>
</tr>
}
</tbody>