Using a partial view - c#

I'm trying to understand how to make partial views. So far I have the following for the partial view, called "_News":
#model Site.Services.News.NewsItem
<div class="bs-callout bs-callout-primary">
<h2>
#Html.DisplayFor(model => item.Title)
</h2>
</div>
And then in the controller I have:
#model IEnumerable<Site.Services.News.NewsItem> - Does this belong here?
...other controller code here...
#foreach(var item in Model)
{
Html.Partial("_News", item);
}
But I'm getting "NullReferenceException" when I try to run the application. What am I doing wrong?
Edit as per comments:
public ActionResult Index()
{
NewsReader newsReader = new NewsReader();
var newsItems = newsReader.GetNewsItems();
return View(newsItems);
}

#model Site.Services.News.NewsItem
<div class="bs-callout bs-callout-primary">
<h2>
#Html.DisplayFor(model => model.Title)
</h2>
</div>
There is a syntax error in your code, it should read model.Title, not item.Title because you are referring to the the model as model in the lambda expression.
I.e. this is the same:
#Html.DisplayFor(x => x.Title)
EDIT:
You also need to put an # symbol before the Html.Partial
#foreach(var item in Model)
{
#Html.Partial("_News", item);
}
See: Html.Partial not rendering partial view

Okay so my mistake, I've solved it, I was missing # on the Html.Partial!
I was surprised that this didn't throw up either an exception or actually display "Html.Partial..." as actual text.
Anyone know why neither of these were the case? If it's not thrown an exception then why wasn't it displayed as plain text?

Related

Save multiple rows simultaneously from the same form - dotnet core

I have a table that has one empty column into which user can enter a comment:
Table
-----
TbMapId | UniqueAdp | Dealership | Line
--------------------------------------------------------------------
1 | [Insert comment here] | Derby | abc123
2 | [Insert comment here] | Keighley | cda345
3 | [Insert comment here] | Manchester | 876ghj
There is a lot of data to comment on, I can't expect a user to open an 'Edit' page and type in a comment one by one. Instead I need user to be able to input a bunch of comments (say 20 at a time against 20 rows) and save them all at one click of submit button.
If you want to jump straight to working solution go to EDIT #2 & look at Rudi's accepted answer
View
<form asp-action="TbMapViewEdit">
<div class="col-lg-6">
<div class="row">
<input type="submit" value="Save" class="btn btn-primary" />
<div class="col-md-12">
<table class="table table-condensed table-bordered table-hover">
<thead>
<tr>
<td><b>TEMP ID</b></td>
<td><b>Map To</b></td>
<td><b>Accounts Code</b></td>
<td><b>Line</b></td>
<td><b>Map Result</b></td>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.TBMapBalancesList.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(Model => Model.TBMapBalancesList[i].TbMapId)
#Html.HiddenFor(Model => Model.TBMapBalancesList[i].TbMapId)
</td>
<td>#Html.EditorFor(Model => Model.TBMapBalancesList[i].UniqueAdp, new { #class = "control-label_DI" })</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalancesList[i].AccountsCode)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalancesList[i].Line)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalancesList[i].MapResult)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</form>
Model
I've learned today that I need to use List to be able to iterate through the lines in table by the use of #for loop (as shown above). before I was trying to use IEnumerable. So I added a definition to the model for public List<TBMapBalances> TBMapBalancesList { get; set; }
public class TbMapViewModel
{
public IEnumerable<ASPNET_Core_1_0.Models.TBMapBalances> TBMapBalances { get; set; }
public IEnumerable<ASPNET_Core_1_0.Models.TBMapUniqueADP> TBMapUniqueADP { get; set; }
public List<TBMapBalances> TBMapBalancesList { get; set; }
[...]
}
Controller:
Now this is where I need the help with, my code doesn't throw any errors at all. When I press Submit nothing happens:
[Authorize]
[HttpPost]
public async Task<IActionResult> TbMapViewEdit(TbMapViewModel tbMapViewModel)
{
if (ModelState.IsValid)
{
foreach (var TbListId in tbMapViewModel.TBMapBalancesList)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.TbMapId = TbListId.TbMapId;
}
}
try
{
_context.Update(tbMapViewModel.TBMapBalances);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw;
}
}
return RedirectToAction("TbMapView");
}
EDIT #1
Changes to View
<form asp-action="TbMapViewEdit">
<div class="col-lg-6">
<div class="row">
<input type="submit" value="Save" class="btn btn-primary" />
<div class="col-md-12">
<table class="table table-condensed table-bordered table-hover">
<thead>
<tr>
<td><b>TEMP ID</b></td>
<td><b>Map To</b></td>
<td><b>Accounts Code</b></td>
<td><b>Line</b></td>
<td><b>Map Result</b></td>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.TBMapBalances.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(Model => Model.TBMapBalances[i].TbMapId)
#Html.HiddenFor(Model => Model.TBMapBalances[i].TbMapId)
</td>
<td>#Html.EditorFor(Model => Model.TBMapBalances[i].UniqueAdp, new { #class = "control-label_DI" })</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalances[i].AccountsCode)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalances[i].Line)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalances[i].MapResult)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</form>
Changes to model
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ASPNET_Core_1_0.Models.TbMapViewModels
{
public class TbMapViewModel
{
public IEnumerable<ASPNET_Core_1_0.Models.TBMapUniqueADP> TBMapUniqueADP { get; set; }
public List<TBMapBalances> TBMapBalances { get; set; }
[...]
}
}
Changes to Controller:
Now this is where I need the help with, my code doesn't throw any errors at all when at the current state - when I press Submit nothing happens and no data gets saved to the database.
however, when you uncomment line _context.Update(tbMapViewModel.TBMapBalances); I get an error that List is not part of any Model and is not found.
Also, below code is something I wrote trying to follow this SO post: update-multiple-records-at-once-in-asp-net-mvc - Initially I was trying to make it Async but I was getting even more errors and couldn't continue. I thought I am going to follow it as closely as possible in hope that it will get me a working starting point.
[Authorize]
[HttpPost]
public IActionResult TbMapViewEdit(TbMapViewModel tbMapViewModel)
{
if (ModelState.IsValid)
{
foreach (var TbListId in tbMapViewModel.TBMapBalances)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.TbMapId = TbListId.TbMapId;
}
}
// _context.Update(tbMapViewModel.TBMapBalances);
_context.SaveChanges();
}
return RedirectToAction("TbMapView");
}
EDIT #2 - A hero to the rescue - big thanks to #RudiVisser for help
First of all if any of you guys are - like me - stuck using .net core 1.0.0
make sure you upgrade to the latest version first (1.1.7 lts). Part of my grief was that I was an USER 1.0 and did not upgrade my installation as fixes and additions kept coming out. Don't be that guy, like I was...
All below is thanks to Rudi's help:
View
#using (Html.BeginForm("TbMapViewEdit", "TbMap"))
{
<div class="col-lg-6">
<div class="row">
<input type="submit" value="Save" class="btn btn-primary" />
<div class="col-md-12">
<table class="table table-condensed table-bordered table-hover">
<thead>
<tr>
<td><b>TEMP ID</b></td>
<td><b>Map To</b></td>
<td><b>Accounts Code</b></td>
<td><b>Line</b></td>
<td><b>Map Result</b></td>
</tr>
</thead>
<tbody>
#Html.EditorFor(m => m.TBMapBalances);
</tbody>
</table>
</div>
</div>
</div>
}
Put your "Method", "Controller" in (Html.BeginForm("TbMapViewEdit", "TbMap")) otherwise the form POST action will be set to the current location.
Model
Truncated for brevity. I have view model with List that I will be saving the data to and one other table just for displaying some info.
public class TbMapViewModel
{
public IEnumerable<ASPNET_Core_1_0.Models.TBMapUniqueADP> TBMapUniqueADP { get; set; }
public List<TBMapBalances> TBMapBalances { get; set; } = new List<TBMapBalances>();
[...]
}
Controller
[Authorize]
[HttpPost]
public IActionResult TbMapViewEdit(TbMapViewModel tbMapViewModel)
{
if (ModelState.IsValid)
{
foreach (var TbListId in tbMapViewModel.TBMapBalances)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.UniqueAdp = TbListId.UniqueAdp;
}
}
_context.SaveChanges();
}
return RedirectToAction("TbMapView");
}
Error that I was making here is that I was trying to replace the key with essentially the copy of itself (Find ID of 1 and set it to ID of 1) instead of picking up on the actual one field that I needed to edit which in my case was UniqueAdp.
Then came the new thing to me, which was Editor Template:
Editor Template
Create a folder called EditorTemplates in 'Shared' Folder under your 'Views' folder with the exact name of the model that you intend to edit. In my case the model was called TBMapBalances so I created a TBMapBalances.cshtml file inside the newly created folder, then pasted this (this was originally in my main view file):
#model ASPNET_Core_1_0.Models.TBMapBalances
<tr>
<td>
#Html.DisplayFor(Model => Model.TbMapId)
#Html.HiddenFor(Model => Model.TbMapId)
</td>
<td>#Html.EditorFor(Model => Model.UniqueAdp, new { #class = "control-label_DI" })</td>
<td>#Html.DisplayFor(Model => Model.AccountsCode)</td>
<td>#Html.DisplayFor(Model => Model.Line)</td>
<td>#Html.DisplayFor(Model => Model.MapResult)</td>
</tr>
By the way the new { #class = "control-label_DI" } is there to supposedly add class to each created input field. This doesn't seem to work in .net core and is left there just as a reminder to myself that I need to do this somehow.
Research:
Update multiple records at once in asp.net mvc
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms
http://www.binaryintellect.net/articles/b1e0b153-47f4-4b29-8583-958aa22d9284.aspx
How to bind an Array in MVC Core
https://www.red-gate.com/simple-talk/dotnet/asp-net/model-binding-asp-net-core/
ASP.NET Core 1.0 POST IEnumerable<T> to controller
This problem has 2 parts to it, the first is that there needed to be a way to edit collections of data. This can be solved with EditorTemplates, which involves creating a single editor model and then calling #Html.EditorFor(..) on the collection of items you wish to edit.
A minimal sample (Full Fx, not Core) is available on Github.
The second problem was with the way the entities were being updated, the property being changed was wrong and hence not saving (the PK was being updated to the PK) and the entity was being attached when it's already tracked.
foreach (var TbListId in tbMapViewModel.TBMapBalancesList)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.TbMapId = TbListId.TbMapId;
}
}
try
{
_context.Update(tbMapViewModel.TBMapBalances);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw;
}
It's important to remember what Entity Framework does for you when you retrieve a model from the database. It is automatically tracked by the context, and so it's already attached and ready to update, anything you change will be automatically tracked and subsequently saved.
The call to _context.Update(..) tries to attach the non-tracked models (from your POSTed data) to the context based on ID, but because you've already pulled that ID out (in your .Where(..).FirstOrDefault(..)) it's already tracked, and so blows up.
Also given that this is EFC 1.0 and you have no .Find(..) method, using .SingleOrDefault(..) is probably a better method to use on a primary key field.
Your rewritten code could be as so:
foreach (var postedModel in tbMapViewModel.TBMapBalancesList)
{
var dbModel = _context.TBMapBalances.SingleOrDefault(p => p.TbMapId == postedModel.TbMapId);
if (dbModel != null)
{
dbModel.UniqueAdp = postedModel.UniqueAdp;
}
}
await _context.SaveChangesAsync();
For posterity though I wouldn't recommend it for security reasons, if you wanted to attach your whole posted model to the context (based on ID) and update it, you can do so with code similar to your original, removing the foreach loop:
_context.UpdateRange(tbMapViewModel.TBMapBalances);
await _context.SaveChangesAsync();
(I don't recommend it because everything that was posted will then be set in the database, from experience it's advisable to only set the fields you're expecting to update as per the first code set. It should, however, be quicker than the foreach loop given that you're not loading from the database and saving back in, only the latter)
Do you have the inputs for the comments already built into the razor page? I do not see them. What you would want to do is create a form with the input types that you want for each item in the loop inside the loop. Each form would then reference the iterator as a hidden value to pass when posted. If the loop is foreach(var item in Model.items){} you would have a form containing the inputs in that block with a hidden input that looks like <input type="hidden" name="index" value="#item.index"/> This will allow you to post with whatever identifier you need to attach that specific form data to the correct model.
See this answer for the structure of the form inside the loop Multiple forms on one MVC form, created with a loop, only the first submits data

Insert items to List in Razor with Model [duplicate]

I have added a button in my view. When this button is clicked partial view is added. In my form I can add as much partial view as I can. When Submitting this form data I am unable to send all the partial view data to controller.
I have made a different model having all the attributes and I have made a list of that model to my main model. Can anyone please give me some trick so that I can send all the partial view content to my controller?
In My View
<div id="CSQGroup">
</div>
<div>
<input type="button" value="Add Field" id="addField" onclick="addFieldss()" />
</div>
function addFieldss()
{
$.ajax({
url: '#Url.Content("~/AdminProduct/GetColorSizeQty")',
type: 'GET',
success:function(result) {
var newDiv = $(document.createElement("div")).attr("id", 'CSQ' + myCounter);
newDiv.html(result);
newDiv.appendTo("#CSQGroup");
myCounter++;
},
error: function(result) {
alert("Failure");
}
});
}
In My controller
public ActionResult GetColorSizeQty()
{
var data = new AdminProductDetailModel();
data.colorList = commonCore.getallTypeofList("color");
data.sizeList = commonCore.getallTypeofList("size");
return PartialView(data);
}
[HttpPost]
public ActionResult AddDetail(AdminProductDetailModel model)
{
....
}
In my Partial View
#model IKLE.Model.ProductModel.AdminProductDetailModel
<div class="editor-field">
#Html.LabelFor(model => model.fkConfigChoiceCategorySizeId)
#Html.DropDownListFor(model => model.fkConfigChoiceCategorySizeId, Model.sizeList, "--Select Size--")
#Html.ValidationMessageFor(model => model.fkConfigChoiceCategorySizeId)
</div>
<div class="editor-field">
#Html.LabelFor(model => model.fkConfigChoiceCategoryColorId)
#Html.DropDownListFor(model => model.fkConfigChoiceCategoryColorId, Model.colorList, "--Select Color--")
#Html.ValidationMessageFor(model => model.fkConfigChoiceCategoryColorId)
</div>
<div class="editor-field">
#Html.LabelFor(model => model.productTotalQuantity)
#Html.TextBoxFor(model => model.productTotalQuantity)
#Html.ValidationMessageFor(model => model.productTotalQuantity)
</div>
Your problem is that the partial renders html based on a single AdminProductDetailModel object, yet you are trying to post back a collection. When you dynamically add a new object you continue to add duplicate controls that look like <input name="productTotalQuantity" ..> (this is also creating invalid html because of the duplicate id attributes) where as they need to be <input name="[0].productTotalQuantity" ..>, <input name="[1].productTotalQuantity" ..> etc. in order to bind to a collection on post back.
The DefaultModelBinder required that the indexer for collection items start at zero and be consecutive, or that the form values include a Index=someValue where the indexer is someValue (for example <input name="[ABC].productTotalQuantity" ..><input name="Index" value="ABC">. This is explained in detail in Phil Haack's article Model Binding To A List. Using the Index approach is generally better because it also allows you to delete items from the list (otherwise it would be necessary to rename all existing controls so the indexer is consecutive).
Two possible approaches to your issue.
Option 1
Use the BeginItemCollection helper for your partial view. This helper will render a hidden input for the Index value based on a GUID. You need this in both the partial view and the loop where you render existing items. Your partial would look something like
#model IKLE.Model.ProductModel.AdminProductDetailModel
#using(Html.BeginCollectionItem())
{
<div class="editor-field">
#Html.LabelFor(model => model.fkConfigChoiceCategorySizeId)
#Html.DropDownListFor(model => model.fkConfigChoiceCategorySizeId, Model.sizeList, "--Select Size--")
#Html.ValidationMessageFor(model => model.fkConfigChoiceCategorySizeId)
</div>
....
}
Option 2
Manually create the html elements representing a new object with a 'fake' indexer, place them in a hidden container, then in the Add button event, clone the html, update the indexers and Index value and append the cloned elements to the DOM. To make sure the html is correct, create one default object in a for loop and inspect the html it generates. An example of this approach is shown in this answer
<div id="newItem" style="display:none">
<div class="editor-field">
<label for="_#__productTotalQuantity">Quantity</label>
<input type="text" id="_#__productTotalQuantity" name="[#].productTotalQuantity" value />
....
</div>
// more properties of your model
</div>
Note the use of a 'fake' indexer to prevent this one being bound on post back ('#' and '%' wont match up so they are ignored by the DefaultModelBinder)
$('#addField').click(function() {
var index = (new Date()).getTime();
var clone = $('#NewItem').clone();
// Update the indexer and Index value of the clone
clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
clone.html($(clone).html().replace(/"%"/g, '"' + index + '"'));
$('#yourContainer').append(clone.html());
}
The advantage of option 1 is that you are strongly typing the view to your model, but it means making a call to the server each time you add a new item. The advantage of option 2 is that its all done client side, but if you make any changes to you model (e.g. add a validation attribute to a property) then you also need to manually update the html, making maintenance a bit harder.
Finally, if you are using client side validation (jquery-validate-unobtrusive.js), then you need re-parse the validator each time you add new elements to the DOM as explained in this answer.
$('form').data('validator', null);
$.validator.unobtrusive.parse($('form'));
And of course you need to change you POST method to accept a collection
[HttpPost]
public ActionResult AddDetail(IEnumerable<AdminProductDetailModel> model)
{
....
}

how I can have a controller, which can pass (return) some model to _Layout.cshtml. view

I have a file index.cshtml which is auto generated by EF (code first). Following code is working fine in that file
#model IEnumerable<dyescan.Models.MyMainMenuItem>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ItemText)
</td>
<td>
#Html.DisplayFor(modelItem => item.LinkAction)
</td>
<td>
#Html.DisplayFor(modelItem => item.LinkController)
</td>
</tr>
}
But When I copy pasted the code in _layout.cshtml, I am unable to use this Model there.
Object reference not set to an instance of an object
I get above error in this line #foreach(var item in Model). Following is my code in _Layout.cshtml
#model IEnumerable<dyescan.Models.MyMainMenuItem>
#foreach(var item in Model)
{
<li>#Html.ActionLink(item.ItemText,item.LinkAction,item.LinkController)</li>
}
Update
So I tried to follow th comments this way. I made a Controller Shared (folder name) and in that controller I made an action _Layout (file name of the view). now it returns dbContex.MyMainMenuItems.. to the view
But still unable to make a controller for _Layout.. to pass it a model for my menuitems
When you use model in cshtml view, your Action method return model like :-
public ActionResult Index()
{
var items = themes.Select(o => new SelectListItem {Text = o, Value = o, Selected = o == theme});
return View(items);
}
When Action method return model it is bind to view. so in above code items will be bind to index.cshtml.
If you try to access this model in _Layout.cshtml it will give error.

MVC model Null on post when using Partial view

I have an MVC controller where the model on the post method always comes back as null. I'm not sure if this is because I am using a partial view within the form.
Any idea why the model is not being returned to the controller?
Model
Loading the model
public List<Group> GetStaticMeasures(int businessUnitID)
{
List<Group> groups = ctx.Groups
.Include("Datapoints")
.Where(w => w.BusinessUnitID.Equals(businessUnitID))
.OrderBy(o => o.SortOrder).ToList();
groups.ForEach(g => g.Datapoints = g.Datapoints.OrderBy(d => d.SortOrder).ToList());
return groups;
}
Controller
public ActionResult Data()
{
ViewBag.Notification = string.Empty;
if (User.IsInRole(#"xxx\yyyyyy"))
{
List<Group> dataGroups = ctx.GetStaticMeasures(10);
return View(dataGroups);
}
else
{
throw new HttpException(403, "You do not have access to the data.");
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Data(List<Group> model)
{
ViewBag.Notification = string.Empty;
if (User.IsInRole(#"xxx\yyyyyy"))
{
if (ModelState.IsValid)
{
ctx.SaveChanges(model);
ViewBag.Notification = "Save Successful";
}
}
else
{
throw new HttpException(403, "You do not have access to save the data.");
}
return View(model);
}
Main view
#model List<Jmp.StaticMeasures.Models.Group>
<div class="row">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="large-12">
<div class="large-8 large-centered columns panel">
#foreach (var g in #Model)
{
<h2>#g.Name</h2>
foreach (var d in g.Datapoints)
{
#Html.Partial("Measures", d)
}
<hr />
}
<input type="submit" class="button" value="Save Changes"/>
</div>
</div>
}
</div>
Partial View
#model Jmp.StaticMeasures.Models.Datapoint
#Html.HiddenFor(d => d.ID)
#Html.HiddenFor(d => d.Name)
#Html.HiddenFor(d => d.SortOrder)
#Html.DisplayTextFor(d => d.Name)
#Html.EditorFor(d => d.StaticValue)
#Html.ValidationMessageFor(d => d.StaticValue)
Rendered Html showing consecutive IDs
As you've rightly noted, this is because you're using a partial. This is happening because Html.Partial has no idea that it's operating on a collection, so it doesn't generate the names for your form elements with your intention of binding to a collection.
However, the fix in your case appears to be fairly straightforward. Rather than using Html.Partial, you can simply change your partial into an EditorTemplate and call Html.EditorFor on that template instead. Html.EditorFor is smart enough to know when it's handling a collection, so it will invoke your template for each item in the collection, generating the correct names on your form.
So to do what you need, follow these steps:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates). The name is important as it follows a convention for finding templates.
Place your partial view in that folder. Alternatively, put it in the Shared\EditorTemplates folder.
Rename your partial view to Datapoint.cshtml (this is important as template names are based on the convention of the type's name).
Now the relevant view code becomes:
// Note: I removed # from Model here.
#foreach (var g in Model)
{
<h2>#g.Name</h2>
#Html.EditorFor(m => g.DataPoints)
<hr />
}
This ensures the separation of your views, as you had originally intended.
Update per comments
Alright, so as I mentioned below, the problem now is that the model binder has no way of associating a DataPoint with the correct Group. The simple fix is to change the view code to this:
for (int i = 0; i < Model.Count; i++)
{
<h2>#Model[i].Name</h2>
#Html.EditorFor(m => m[i].DataPoints)
<hr />
}
That will correctly generate the names, and should solve the model binding problem.
OP's addendum
Following John's answer I also included the missing properties on the Group table as HiddenFor's which game me the model back on the post.
#for (int i = 0; i < Model.Count(); i++)
{
#Html.HiddenFor(t => Model[i].ID)
#Html.HiddenFor(t => Model[i].BusinessUnitID)
#Html.HiddenFor(t => Model[i].SortOrder)
#Html.HiddenFor(t => Model[i].Name)
<h2>#Model[i].Name</h2>
#Html.EditorFor(m => Model[i].Datapoints)
<hr />
}
Update 2 - Cleaner solution
My advice for using an EditorTemplate for each DataPoint also applies to each Group. Rather than needing the for loop, again sprinkling logic in the view, you can avoid that entirely by setting up an EditorTemplate for Group. Same steps apply as above in terms of where to put the template.
In this case, the template would be Group.cshtml, and would look as follows:
#model Jmp.StaticMeasures.Models.Group
<h2>#Model.Name</h2>
#Html.EditorFor(m => m.DataPoints)
<hr />
As discussed above, this will invoke the template for each item in the collection, which will also generate the correct indices for each Group. Your original view can now be simplified to:
#model List<Jmp.StaticMeasures.Models.Group>
#using (Html.BeginForm())
{
// Other markup
#Html.EditorForModel();
}
Binder can't bind to list of objects if it is returned like this. Yes, partial is your problem. You need to specify a number within your form for ID's.
Do something like this:
// pseudocode
#model List<Jmp.StaticMeasures.Models.Group>
<div class="row">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="large-12">
<div class="large-8 large-centered columns panel">
for(int i; i<Model.Count; i++)
{
<h2>#g.Name</h2>
#Html.HiddenFor(d => Model[i].Id)
#Html.HiddenFor(d => Model[i].Name)
#Html.HiddenFor(d => Model[i].SortOrder)
#Html.DisplayTextFor(d => Model[i].Name)
#Html.EditorFor(d => Model[i].StaticValue)
#Html.ValidationMessageFor(d => Model[i].StaticValue)
<hr />
}
<input type="submit" class="button" value="Save Changes"/>
</div>
</div>
}
</div>
See more details about binding to a list in Haack's blog
You are getting a null model because of the way the model binder handles collections.
Your partial view is rendering those inputs as for example:
<input type="hidden" name="ID" value="1"/>
...
And then repeating that for each entry in your List<Group>. Unfortunately the model binder won't know how to handle that and you'll get a null value.
The way your inputs have to look is:
<input type="hidden" name="groups[0].ID" value="1"/>
...
<input type="hidden" name="groups[1].ID" value="2"/>
There can't be a break in the numbering. One way to get this is to rewrite the way you use the Html.xxxFor methods, e.g.: iterate over the list and do this:
#Html.HiddenFor(d => Model[i].Id)
Here are two resources that explain this in detail and provide yet other examples of how to make the model binder work with collections:
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/

ASP.NET MVC3 C# - foreach

I am confused as to how to implement the following in my current foreach:
#foreach
(var post in Model."table".Where(w => w.Private_ID == 1).OrderBy(o => o.Date))
{
<div class ="post">
<fieldset>
<p class="post_details">At #post.Post_Date By #post.Username</p>
#post.Post_Desc
</fieldset>
</div>
}
so that post.Username will NOT show if #post.anon is TRUE (and so that it will say "Anonymous")
Thanks in advance for any advice/help/suggestions.
You should be able to do something along the lines of:
#(post.anon ? "Anonymous" : post.Username)
Though I would consider doing most of this logic in the C#, rather than leaving it to the view (therefore, creating a specific view model with all of the logic already done. Meaning you can just loop through and not have to do any additional thinking:
#foreach(var post in Model.Posts)
{
<div class ="post">
<fieldset>
<p class="post_details">At #post.Post_Date By #post.Poster</p>
#post.Post_Desc
</fieldset>
</div>
}
Where #post.Poster in the above example is already preset with anonymous if it is required.
Try this:
#foreach(var post in Model."table".Where(w => w.Private_ID == 1).OrderBy(o => o.Date))
{
<div class ="post">
<fieldset>
<p class="post_details">At #post.Post_Date By (#post.Anon == true ? "Anonymous" : #post.Username)</p>
#post.Post_Desc
</fieldset>
</div>
}
EDIT: Sorry, the line should have said: #(post.Anon == true ? "Anonymous" : post.Post_Desc)

Categories