Refer to property within collection in MVC view, using Razor - asp.net-mvc

I have a model that looks somewhat like this:
public class MyClass {
public string Id { get; set; }
public List<SubItem> SubItems { get; set; }
}
public class SubItem {
public string Key { get; set; }
public string Value { get; set; }
}
In my view, I want to submit form data to MyClass, so I can create an object of MyClass. It looks like this:
#model Models.MyClass
#using (Html.BeginForm()){
<div>
#Html.DisplayFor(model => model.Id): #Html.EditorFor(model => model.Id)
</div>
<div>
#Html.DisplayFor(model => ???): #Html.EditorFor( ??? )
</div>
<input type="submit" value="create"/>
}
You see the question marks (???) where I am in doubt. How do I get to add to this collection? I know it is a sub form of sorts, but how do I do it without much complication. If I needed to show the items, I would do a foreach(var item in Model.SubItems) { ... }. But this is different. How do I handle this?

It's really not different than displaying each item individually:
#for (int i=0; i<Model.SubItems.Length; i++)
{
<div>
#Html.DisplayFor(m => m.SubItems[i].Key): #Html.EditorFor(m => m.SubItems[i].Key)
</div>
<div>
#Html.DisplayFor(m => m.SubItems[i].Value): #Html.EditorFor(m => m.SubItems[i].Value)
</div>
}
UPDATE
Changed code above to make sure names and index values are correctly generated. Also, this will now work with scenario of no initial items, as well. Just change the i<Model.SubItems.Length condition to i<3, or whatever number of iterations you'd like.

Related

Why does Razor view add "CS$<>8__locals1" prefix to input names when accessing indexer of model assigned to local variable?

I'm building a simple ASP.NET MVC 5.2.3.0 view that displays a user name and a checkbox list of roles. You can check or uncheck the checkboxes to control which roles the user is in. Here is my ViewModel:
public class EditUserVM
{
public string Id { get; set; }
public string UserName { get; set; }
public List<UserRoleVM> Roles { get; set; }
}
public class UserRoleVM
{
public string RoleId { get; set; }
public string RoleName { get; set; }
public bool IsMember { get; set; }
}
And it's displayed like this:
#model VMContainer<EditUserVM>
#* omitted for exmaple *#
var userVM = Model.ViewModel;
<div class="panel panel-primary">
<div class="panel-heading"><h4 style="display:inline">User: #userVM.UserName</h4></div>
<div class="panel-body">
#Html.ValidationSummary(false, "", new { #class = "text-danger" })
<h4>Users Roles</h4>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.HiddenFor(m => userVM.Id)
#Html.HiddenFor(m => userVM.UserName)
<table class="table table-bordered table-striped">
<thead><tr><th></th><th>Role</th></tr></thead>
<tbody>
#for (int x = 0; x<userVM.Roles.Count; x++){
<tr>
<td>
#Html.HiddenFor(m => userVM.Roles[x].RoleId)
#Html.CheckBoxFor(m => userVM.Roles[x].IsMember)
</td>
<td>#Html.DisplayFor(m => userVM.Roles[x].RoleName)</td>
</tr>
}
</tbody>
</table>
<div>
<input type="submit" value="Save" class="btn btn-success" />
#Html.ActionLink("Cancel", "Users", null, new { #class = "btn btn-default" })
</div>
</div>
}
Notice the model type is VMContainer. This is a wrapper used by my views so that in case there is an error generating the inner T ViewModel, it can display an error instead of the normal view content. To make it easier to access the EditUserVM and because that is all I need to submit, not the VMContainer wrapper, I assign it to a local variable var userVM = Model.ViewModel;. I've used this pattern a lot in previous projects. The problem is when I inspect the page in developer tools, all of my inputs are named wrong. They all get a prefix for what looks like a generated class. For example, #Html.HiddenFor(m => userVM.Id) renders:
Through trial and error, I narrowed this down to not happening if I remove the for loop. That is with the for loop commented out, the remaining inputs are named correctly. Through further trial and error I discovered that what really triggers it is accessing the Roles indexer using a variable. E.G. If I remove the loop and just do this, it produces the wrong name for all of the inputs:
#Html.HiddenFor(m => userVM.Id)
#Html.HiddenFor(m => userVM.UserName)
#{ int x = 0; }
#Html.HiddenFor(m => userVM.Roles[x].RoleId);
It does not generate the wrong names if I use the constant [0] in place of [x] or m.ViewModel.Roles[x] instead of userVM.Roles[x]. So to try to spell it out completely, it seems to occur when accessing the indexer of a local variable's descendant using a variable for the index parameter.
I found this or something similar acknowledged for MVC 6 (at the time) but I don't understand the problem. I'm a little surprised this still exists if it is in fact a bug. Although it's hard to describe, it doesn't seem like it would be a totally uncommon scenario. Can someone explain what is going on and hopefully offer a fix? I prefer to only post an EditUserVM to my action when the form submits.
#for (int x = 0; x<userVM.Roles.Count; x++){
A workaround could be declaring the variable x in the top of your page.
Source: https://github.com/aspnet/Mvc/issues/2890

Set value from model in a label using html helper

I have given view model.I have to set the value of Amount(which is getting retrieved from table using linq) on a label using Html helper.
public class AllocationViewModel
{
public long ID { get; set; }
public string Name { get; set; }
public double Amount { get; set; }
}
Code for view page:--
#model Assetry.Controllers.AllocationViewModel
#using (Html.BeginForm("Index", "Deal", FormMethod.Post))
{
#Html.DisplayFor(model => model.Amount)
}
Something like this maybe?
#model AllocationViewModel
#Html.DisplayFor(model => model.Amount)
Try this,
#Html.Label(Model.Amount)
or
IF you want value in model,
#Html.LabelFor(model => model.Amount)
I think in your case you don't need a display template, except you want to represent a double in a sophisticated manner using a template.
Try something like that (if you want to just display the amount):
<span>#Model.Amount</span>
To edit:
#Html.LabelFor(m => m.Amount)
#Html.TextBoxFor(m => m.Amount)

ViewModel IEnum<> property is returning null (not binding) when contained in a partial view?

I have a ViewModel that contains a Product type and an IEnumerable< Product > type. I have one main view that displays the ViewModel.Product at the top of the page but then I have a partial view that renders the ViewModel.IEnumerable< Product > data. On the post the first level product object comes back binded from the ViweModel whereas the ViewModel.IEnumerable< Product > is coming back null.
Of course if I remove the partial view and move the IEnumerable< Product > view to the main View the contents comes back binded fine. However, I need to put these Enumerable items in a partial view because I plan on updating the contents dynamically with Ajax.
Why is the IEnumerable< Prouduct> property not getting binded when it's placed in a partial view? Thx!
Models:
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductIndexViewModel
{
public Product NewProduct { get; set; }
public List<Product> Products { get; set; }
}
public class BoringStoreContext
{
public BoringStoreContext()
{
Products = new List<Product>();
Products.Add(new Product() { ID = 1, Name = "Sure", Price = (decimal)(1.10) });
Products.Add(new Product() { ID = 2, Name = "Sure2", Price = (decimal)(2.10) });
}
public List<Product> Products { get; set; }
}
Views:
Main index.cshtml:
#model ViewModelBinding.Models.ProductIndexViewModel
#using (#Html.BeginForm())
{
<div>
#Html.LabelFor(model => model.NewProduct.Name)
#Html.EditorFor(model => model.NewProduct.Name)
</div>
<div>
#Html.LabelFor(model => model.NewProduct.Price)
#Html.EditorFor(model => model.NewProduct.Price)
</div>
#Html.Partial("_Product", Model.Products)
<div>
<input type="submit" value="Add Product" />
</div>
}
Parial View _Product.cshtml:
#model List<ViewModelBinding.Models.Product>
#for (int count = 0; count < Model.Count; count++)
{
<div>
#Html.LabelFor(model => model[count].ID)
#Html.EditorFor(model => model[count].ID)
</div>
<div>
#Html.LabelFor(model => model[count].Name)
#Html.EditorFor(model => model[count].Name)
</div>
<div>
#Html.LabelFor(model => model[count].Price)
#Html.EditorFor(model => model[count].Price)
</div>
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
BoringStoreContext db = new BoringStoreContext();
ProductIndexViewModel viewModel = new ProductIndexViewModel
{
NewProduct = new Product(),
Products = db.Products
};
return View(viewModel);
}
[HttpPost]
public ActionResult Index(ProductIndexViewModel viewModel)
{
// work with view model
return View();
}
}
When you use #Html.Partial("_Product", Model.Products) your input fields do not have correct names. For example instead of:
<input type="text" name="Products[0].ID" />
you get:
<input type="text" name="[0].ID" />
Just look at your generated markup and you will see the problem. This comes from the fact that when you use Html.Partial the navigational context is not preserved. The input fields names are not prefixed with the name of the collection - Products and as a consequence the model binder is not able to bind it correctly. Take a look at the following blog post to better understand the expected wire format.
I would recommend you using editor templates which preserve the context. So instead of:
#Html.Partial("_Product", Model.Products)
use:
#Html.EditorFor(x => x.Products)
and now move your _Product.cshtml template to ~/Views/Shared/EditorTemplates/Product.cshtml. Also since the editor template automatically recognizes that the Products property is an IEnumerable<T> it will render the template for each item of this collection. So your template should be strongly typed to a single Product and you can get rid of the loop:
#model Product
<div>
#Html.LabelFor(model => model.ID)
#Html.EditorFor(model => model.ID)
</div>
<div>
#Html.LabelFor(model => model.Name)
#Html.EditorFor(model => model.Name)
</div>
<div>
#Html.LabelFor(model => model.Price)
#Html.EditorFor(model => model.Price)
</div>
Now everything works by convention and it will properly bind.

Multiple partial views based on same model

I have something like this:
Main view:
#model AuthorViewModel
#using (Html.BeginForm("Action", "Controller", FormMethod.Post, new { id="someId" })) {
#Html.LabelFor(model => model.Name);
#Html.EditorFor(model => model.Name);
#Html.ValidationMessageFor(model => model.Name);
<label> Book </label>
#{Html.RenderPartial("_BookView", new BookViewModel());}
<label>One more book...</label>
#{Html.RenderPartial("_BookView", new BookViewModel());}
}
Partial view:
#model BookViewModel
#Html.LabelFor(model => model.Title);
#Html.EditorFor(model => model.Title);
#Html.ValidationMessageFor(model => model.Title);
AuthorViewModel:
public class AuthorViewModel
{
[Required]
[DataType(DataType.Text)]
public String Name { get; set; }
}
BookViewModel:
public class BookViewModel
{
[Required]
[DataType(DataType.Text)]
public String Title { get; set; }
}
So when it renders - it looks right, but validation is the same for all books. An I need to have a lot of books(say to add them dynamically) for author and each one have to be independent and "validatable".
How can I perform such behaviour?
I would have a collection of BookViewModel in your AuthorViewModel. That way the names and ids will be unique.
You could update your AuthorViewModel to have a List of BookViewModel. In the View, iterate over the list and create the necessary fields for the booktitles.
You're trying to model bind to a list.
Its pretty simple to implement, have a look at Phil Haacks post here.
He uses the old mvc views, but the same idea works fine for razor

Why is my ViewModel empty on [HttpPost]? .NET MVC 3

I'm trying my hardest to use ViewModels correctly in my web application, but I'm running into various problems. One of which, is if I set a breakpoint just after I post using a Create action, my viewModel hasn't stored any of my form values. I must be doing something wrong, but I've tried a few things. Including the code below, where I name the form items the same as the viewModel fields to see if that helps.
I'm also wondering what exactly properties in your viewmodel should represent. I've seen people use different things in blog posts and whatnot.
If the view is going to render a select list, I'm under the impression the viewmodel should hold an IEnumerable SelectListItem for this as below. Yet I've seen people use IEnumerable Entity instead, to represent the type the select list represents.
Can anybody shed some light on this for me? I scrapped my entire business logic last night so I could start a fresh and try and do it correctly.
My ViewModel:
public class ServerCreateViewModel
{
public int Id { get; set; }
// CompanyName represents a field in the Company model. I did this to see if
// it would help with model binding. Beforehand it was Companies to represent the type. I've done the same for the rest of them, so I wont comment on this again.
public IEnumerable<SelectListItem> CompanyName { get; set; }
// Represents the Game model.
public IEnumerable<SelectListItem> GameTitle { get; set; }
//Represents the Location model, etc...
public IEnumerable<SelectListItem> City { get; set; }
public IEnumerable<SelectListItem> NumberOfPlayers { get; set; }
public IEnumerable<SelectListItem> CurrencyAbbreviation { get; set; }
}
My Controller action:
public ActionResult Create()
{
var viewModel = new ServerCreateViewModel();
viewModel.CompanyName = new SelectList(_dataService.Companies.All(), "Id", "CompanyName");
viewModel.GameTitle = new SelectList(_dataService.Games.All(), "Id", "GameTitle");
viewModel.City = new SelectList(_dataService.Locations.All(), "Id", "City");
viewModel.NumberOfPlayers = new SelectList(_dataService.ServerPlayers.All(), "Id", "NumberOfPlayers");
return View(viewModel);
}
[HttpPost]
public ActionResult Create(FormCollection collection, ServerCreateViewModel viewModel)
{
try
{ // I put a breakpoint in here to check the viewModel values.
// If I dont pass the viewModel into the constructor, it doesnt exist.
// When I do pass it in, its empty.
return Content("Success");
}
catch
{
return Content("Fail");
}
}
My View:
#model GameserverCompare.ViewModels.Server.ServerCreateViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Server</legend>
#Html.HiddenFor(m => m.Id)
<div class="editor-label">
#Html.LabelFor(model => model.CompanyName)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.CompanyName, Model.CompanyName)
#Html.ValidationMessageFor(model => model.CompanyName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.GameTitle)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.GameTitle, Model.GameTitle)
#Html.ValidationMessageFor(model => model.GameTitle)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.City)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.City, Model.City)
#Html.ValidationMessageFor(model => model.City)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.NumberOfPlayers)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.NumberOfPlayers, Model.NumberOfPlayers)
#Html.ValidationMessageFor(model => model.NumberOfPlayers)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Since you're using SelectList properties in the form model, you will need to have a different model to represent the selected values in those lists:
public class ServerCreatePostbackModel
{
public int Id { get; set; }
// CompanyName represents a field in the Company model.
public string CompanyName { get; set; }
// Represents the Game model.
public string GameTitle { get; set; }
//Represents the Location model, etc...
public string City { get; set; }
public int NumberOfPlayers { get; set; }
public string CurrencyAbbreviation { get; set; }
}
Have your HttpPost action take one of these as its argument.
Oh, and be sure to use HiddenFor for the Id property, so it gets sent back with the other data.

Resources