Creating multiple TextBoxes in the view from an string array in Model - c#

I have an array of string which I want to show a separate Textbox for each one of them in the View.
I tried to use it like this (which Items is of type string[]):
#foreach(var x in Model.Items) {<input asp-for="x" />}
But this does not work. I can of course go the old way:
#foreach(var x in Model.Items) {<input name="items" value="x" />}
but I want to know how to do it MVC way.

For MVC, I would highly recommend putting the string array inside a viewmodel:
public class SomeViewModel
{
public string[] Items { get; set; }
}
And then point the model to SomeViewModel. And, in the UI, do:
#for (var i = 0; i < Items.Length; i++) {
<input name="#Html.NameFor(m => m.Items[i])" value="#Model.Items[i]" />
}
or use Html.TextBoxFor(m => m.Items[i]) as an alternative to client-side input.

Related

Loop through numbered properties to generate inputs in Razor Pages

I have a model with 34 numbered properties in it as shown below
Public Class ViewModel
{
public string RatingCategory01 { get; set; }
public string RatingCategory02 { get; set; }
public string RatingCategory03 { get; set; }
//...and so on until category #34
}
Rather than code an input for each category in Razor Pages, I would like to use a loop to iterate through all the categories and generate the appropriate control groups. I have tried the code below:
<tbody>
#for (var i = 1; i < 35; i++)
{
string n;
#if (i > 0 && i < 10)
{
n = "RatingCategory0" + i.ToString();
}
else
{
n = "RatingCateogry" + i.ToString();
}
<tr>
<td>
<label asp-for="#string.Format("RatingCategory" + n)" class="control-label"></label>
</td>
<td>
<select asp-for="#string.Format("RatingCategory" + n)" asp-items="Model.CategoryRatingSelectList">
<option value="">Select</option>
</select>
</td>
<td>
<input asp-for="#string.Format("RemedialTime" + n)" class="form-control" />
</td>
</tr>
}
</tbody>
When I build the project and navigate to the page, I get this error:
InvalidOperationException: Templates can be used only with field
access, property access, single-dimension array index, or
single-parameter custom indexer expressions.
I'm not sure if I am on the right track here. I would really like to create a loop to generate these inputs so make future maintenance and changes easier. It's probably pretty obvious from my code/question that I am pretty new to this, so any help is appreciated.
EDIT TO ADD SOLUTION:
I used the solution provided by Ed Plunkett which I have checked below. I altered it a bit and ended up creating a new class called 'Rating' because I found that in practice I needed a more complex object. Inside my view is now
public List<Rating> Ratings = { get; set; }
In the controller, I use a loop to add as many empty ratings as I need to the list depending on the number I need.
for (var i = 0; i < 34; i++)
{
vm.Ratings.Add(new Rating());
}
Though this will likely be updated to use something other than a hard-coded number as the application evolves.
Finally, I used a loop in the view to create a group of controls for every Rating in my List. In this case it is a TableRow containing different controls in different columns:
#for (var i = 0; i < Model.Ratings.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(model => model.Ratings[i].Category)
</td>
<td>
<div class="form-group">
<select asp-for="Ratings[i].RatingValue" asp-items="Model.CategoryRatingSelectList">
<option value="">Select</option>
</select>
</div>
</td>
<td>
<input asp-for="Ratings[i].RemediationMinutes" class="form-control" />
</td>
</tr>
}
I've found that the data in this group of inputs can be bound as a List by simply including
List<Rating> Ratings
in the parameters on whichever method runs when the form is submitted.
This is what you want instead of those 34 properties and their implied 34 RemedialTime siblings:
public List<String> RatingCategory { get; set; } = new List<String>();
public List<String> RemedialTime { get; set; } = new List<String>();
If you have 34 of something and the names differ only by an index number, that's a collection, not 34 distinct properties with sequentially numbered names. Then you can enumerate the 34 items with a foreach loop, or index them individually as RatingCategory[0] through RatingCategory[33]. In C#, collection indexes start at zero, so the first one is 0 and the thirty-fourth one is 33. You get used to it.
You should also look up what String.Format() does. String.Format("Foo" + 1) is exactly the same as "Foo" + 1.
You could convert your model class to dictionary;
var viewModel = new ViewModel()
{
RatingCategory01 = "a",
RatingCategory02 = "b",
RatingCategory03 = "c"
};
var dictionaryModel = viewModel.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(viewModel, null));
Then you can iterate the dictionary in the view.

ASP.NET MVC 4.5 Map Partial View to Main View on Form submit

Hello Mighty Stackoverflowers,
I'm currently working on an ASP.NET MVC 4.5 application. I need to map the input values from my partial view to my main View Model, when I submit the create form.
In my View "Create.cshtml" I call a partial view "_SwotPart.cshtml". I pass a part of my ViewModel to the Partial View, like this:
Create.cshtml:
#model MyCoolApp.BLL.Models.MainVm
#foreach (var swot in Model.Swots)
{
<tr>
#foreach (var swotPart in swot.SwotParts)
{
#Html.Partial("~/Views/Shared/_SwotPart.cshtml", swotPart)
}
</tr>
}
My partial View looks as follows, _SwotPartial.cshtml :
<td class="form-group">
#Html.TextAreaFor(model => model.Label, htmlAttributes: new { Name = nameField, ID = nameField, #class = "form-control", placeholder = Model.SwotTypeId.GetLabel() })
</td>
Anyways, when I submit my form, the values from the partial view never arrive in the controller.
Do you have any ideas how to map this properly?
Thanks!
The problem is in the input names that will be generated the way you're currently trying to achieve this. Razor needs the context of the entire list, or at least the item's position in it, in order to generate correct input names. In other words, the easiest way to solve your issue (with a caveat) is:
#for (var i = 0; i < Model.Swots.Count(); i++)
{
...
#for (var j = 0; j < Model.Swots[i].SwotParts.Count(); j++)
{
if (Model.Swots[i].SwotParts[j].SwotTypeId == SwotType.InternalHelpful || Model.Swots[i].SwotParts[j].SwotTypeId == SwotType.InternalHarmful)
{
#Html.Partial("~/Views/Shared/_SwotPart.cshtml", Model.Swots[i].SwotParts[j])
}
}
...
Then, the partial has the correct context to work with and your inputs will be named like Swots[0].SwotParts[0].Label, which the modelbinder will be able to work with.
However, the caveat here is that you're splitting this list into two loops. That's still not going to work, as you're effectively messing with the overall context of the item(s) position within the model. To fix that, you should split your list in your model, which is better anyways, as you can remove this business logic from your view:
public class SwotVm
{
...
public List<SwotPartVm> InternalSwotParts { get; set; }
public List<SwotPartVm> ExternalSwotParts { get; set; }
}
Then, you can simply iterate over each list individually, and the values will naturally post back to the appropriate list.
Given that you're using a partial to render fields for a particular class type, though, you'd be better served by creating an editor template. If you simply move your partial code to the view: Views\Shared\EditorTemplates\SwotPartVm.cshtml, then in your main view, you can just do:
#for (var i = 0; i < Model.Swots.Count(); i++)
{
...
<tr>
<th class="swot-heading">Internal</th>
#Html.EditorFor(m => m.Swots[i].InternalSwotParts)
</tr>
<tr>
<th class="swot-heading">External</th>
#Html.EditorFor(m => m.Swots[i].ExternalSwotParts)
</tr>
}
That's obvious much cleaner, and you can take this concept even further by adding a SwotVm.cshtml editor template, allowing you replace even this little bit of code with just:
#Html.EditorFor(m => m.Swots)
Note: In your SwotVm.cshtml editor template, you would only include the code for a single SwotVm. In other words, not including the for statement.
In order for your application to parse the posted values and properly and bind it to your view model. Names of posted form data needs to be like.
swots[x1].swotParts[x2].label
Where x1 is a number ranging from 0 and up for each swot.
Where x2 is a number ranging from 0 and up for each swot part in swots.
Now when you are posting, the form data names is just label.
Instead of :
#Html.TextAreaFor(model => model.Label, htmlAttributes: new { Name = nameField, ID = nameField, #class = "form-control", placeholder = Model.SwotTypeId.GetLabel() })
try :
<textarea name="swots[x1].swotParts[x2].label" class="form-control" placeholder="#Model.SwotTypeId.GetLabel()" >#Model.Label</textarea>
Don't forget to replace the x1 and x2 with a number.
You can read more about model bindings to collections here.
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

Binding DropDownListFor In foreach , ASP.NET MVC

Here is my code :
ViewModel
public class FooViewModel{
public Guid BarId { set;get }
}
View :
#model IEnumerable<FooViewModel>
#foreach (var c in Model)
{
<div>
#Html.DropDownListFor(o => c.BarId , (List<SelectListItem>)ViewBag.BarCollection)
</div>
}
the problem is DropDownListFor create the options completely but binding doesn't work.
You cannot use a foreach loop to generate controls for items in a collection. If you inspect the html you will see that you have duplicate name attributes without indexers (and also duplicate id attributes which is invalid html). You need a for loop of a custom EditorTemplate for FooViewModel. Using a for loop (your model must implement IList<T>)
#model IList<FooViewModel>
for (int i = 0; i < Model.Count; i++)
{
#Html.DropDownListFor(m => m[i].BarId, ....)
}
Note the html will now be
<select name="[0].BarId" ..>
<select name="[1].BarId" ..>
etc.

Using Razor to return strings in array

I am taking an string[] from my Model and it has about 25ish strings in it at any given time.
#model PostProcessPartSelectionViewModel
#{
var i = 0;
foreach (var part in Model.PartsAllowedAsSeed)
{
<input type="checkbox" id="[#i]" name="PartsAllowedAsSeed" value="#part" />
<span>#part</span>
<br />
i++;
}
}
I set up a #foreach loop in Razor to display a checkbox and label for each string, but when I debug, #part renders to System.Object[]. There are 25 checkboxes with 25 "System.Object[]" labels.
Eventually, I'm going to want to return any checked strings back to the model, but right now I just want to know how I can get Razor to render the actual string value.
Don't use foreach in razor, use a for loop so you can directly bind to your model:
#for (int i = 0; i < #Model.PartsAllowedAsSeed.Length; i++)
{
<input type="checkbox" id="[#i]" name="PartsAllowedAsSeed" />
<span> #Model.PartsAllowedAsSeed[i] </span>
<br />
}
as for your System.Object[], you can do string.Join(", ", #Model.PartsAllowedAsSeed[i]) or some equivalent to meet your needs
My case was pretty specific, so I don't think this will apply to anyone. I had a Object[] with objects in it and I wanted to display each child object as a string with Razor. I used a hack to cast the Object[] to a list, then append brackets to each list entry and reported each string back to Razor. For now, I don't need to bind directly to the model, so I opted to just use a foreach.
Razor Code:
#{
var i = 0;
#foreach (var item in Model.PartsAllowedAsSeed)
{
<input type="checkbox" id="#i" name="PartsAllowedAsSeed" />
<span>#Html.ConvertToArray(item)</span>
<br />
i++;
}
}
Helper Class:
public static MvcHtmlString ConvertToArray(this HtmlHelper htmlHelper, object source)
{
var src = source as IEnumerable;
if (src == null) return MvcHtmlString.Create(string.Empty);
var sourceAsList = src.Cast<Object>().ToList();
var output = new StringBuilder();
output.Append("[");
for (var index = 0; index < sourceAsList.Count; index++)
{
var item = sourceAsList[index];
output.Append(item);
if (index != (sourceAsList.Count - 1)) output.Append(", ");
}
output.Append("]");
return MvcHtmlString.Create(output.ToString());
}

IEnumerable property is null after submit

I am new to MVC and have some difficulties understanding this.
To make it simple, I have a "Person" object and this object has an IEnumerable property called "EmailaddressList".
I have generated an edit page through Visual Studio 2012. The main objects properties, are generated on the edit page with textboxes like Name and LastName.
However the list of e-mail addresses in the IEnumerable list of sub-objects are not generated automatically in my view.
This is OK, I have written that code by hand using a tab for each type of e-mailaddress.
So far so good.
Problem:
When I recieve the model (person object) in my HTTP-Post method, the EmailAddressList is null.
Why is it like this, It was not null when I sent it to the view.
I the tab where the e-mailadresses are listed is in a partial view.
Can anyone give me some tips, is it something I'm missing here?*
View-Code
<div id="tabs">
<ul>
#foreach (var item in Model.EmailAddressList)
{
<li>#Html.Label(item.AddressType)</li>
}
</ul>
#foreach (var item in Model.EmailAddressList)
{
<div id="#item.AddressType">
<p>
#Html.TextBoxFor(s => item.EmailAddress, new { #class = "input-xxlarge" })
</p>
</div>
}
</div>
Controller (recieving method)
Here person.EmailAddressList is null
[HttpPost]
public ActionResult Create(Person person)
{
if (ModelState.IsValid)
{
personRepository.InsertOrUpdate(person);
personRepository.Save();
return RedirectToAction("Index");
}
else
{
return View();
}
}
That's because in order to correctly index your fields (so model binder can do it's work), you have to use a for loop.
First, change your IEnumerable to be a List (so we can use an indexor in the view).
Then change your foreach to be the following for loop:
#for (int i = 0; i < Model.EmailAddressList.Count; i++)
{
<div id="#Model.EmailAddressList[i].AddressType">
<p>
#Html.TextBoxFor(m => m.EmailAddressList[i].EmailAddress, new { #class = "input-xxlarge" })
</p>
</div>
}
Based on your update, the reason this doesn't work is because the default model binder only relies on order for a collection of simple data. When it comes to complex type you need to provide the relevant index per item otherwise it doesn't know which item property your referring to e.g.
#for (int i = 0; i < Model.EmailAddressList.Count; i++) {
Html.TextBoxFor(m => m.EmailAddressList[i].EmailAddress) %>
}
See Phil Haack's article on model binding to a list.
It's due to your elements not being ID'd the correct thing for MVC to pick them up on the post back, what you need is:
#Html.EditorFor(model => model.EmailAddressList);
Then, please refer to my post located here on how to make this look to how you want it to.

Categories