I'm tinkering with a ASP.NET MVC 4 template, and I need guidance on how to design a complex view layout.
I already created a model that is getting data returned by a stored procedure in a SQL Server DB. A view is getting data from the model in an IEnumerable<> object. If you were to view the raw output of the stored procedure it would look something like this:
**Name** **Objects**
John Orange
John Banana
John Apple
I used a view template to create a simple table based on logic below, but as expected, it is rendered exactly like it is stored in the IEnumerable<> object. The name appears on each row of output in the table, like above.
#model IEnumerable<projectname.Models.ObjectsModel>
<table class="table">
...
...
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Objects)
</td>
...
Instead, I want to create a view layout that is not in table form, that would display the Name as a heading, the objects as a bulleted list, and other attributes elsewhere, with no redundant data. My problem is that if I display the items in the IEnumerable<> using #foreach (var item in Model) I end up getting three duplicate entries, since the values in the Name field are not unique.
What is the 'best practice' way to do this? Can someone please point me in the right direction?
Not sure what your controller looks like, or even your view model, but maybe i can point you in the right direction.
foreach is just that a loop of objects and should be used for just that, i have seen many people manipulate foreach loops with little or no success, You should be using a #for loop. besides The for-loop is approximately 2 times faster than the foreach-loop as it only has to call get_Item for every element in the list.
there are a few ways to acomplis what you want, one way would be what #colinD stated above by using LINQ in the controler or viewmodel to pass the proper data. the other way would be somthing similar to the following.
var models = ObjectsModel();
var grouped = models.GroupBy(s => s.ObjectsModel.Name)
.Select(x => x.Select(y => y))
.ToList();
return View(grouped);
#for(int i = 0; i < Model.Count; i++)
{
<h2>#Html.DisplayFor(model => model[i].First().ObjectsModel.Name)</h2>
<ul>
for(int j = 0; j < Model[i].Count; j++)
{
<li>#Html.DisplayFor(model => model[i][j].Object)</li>
}
</ul>
}
I haven't tested the code but I hope this helps you get the right solution you are looking for.
The main thing i'm trying to figure out is how to display this data
without having the #foreach (var item in Model) loop generate
duplicate entries for each row in the SP output.
Usually processing data is done in action, like groupping data, and then make some loop to display. As you want to avoid #foreach I thought using linq. This is just an idea but keep in mind you should separate concerns in MVC. I hope this helps ;)
IEnumerable<ObjectsModel> model = new List<ObjectsModel>()
{
new ObjectsModel(){ Name = "John", Objects = "Orange" },
new ObjectsModel(){ Name = "John", Objects = "Banana" },
new ObjectsModel(){ Name = "John", Objects = "Apple" }
};
var htmlModel = model
.GroupBy(a => a.Name)
.Select(a => new
{
Name = a.Key,
GroupObjects = string.Join("", a.Select(b => $"<li>{b.Objects}</li>"))
})
.Select(a => $"<h1>{a.Name}</h1><ul>{a.GroupObjects}</ul>")
.ToList();
var result = string.Join("", htmlModel); // <h1>John</h1><ul><li>Orange</li><li>Banana</li><li>Apple</li></ul>
Final result:
<h1>John</h1>
<ul>
<li>Orange</li>
<li>Banana</li>
<li>Apple</li>
</ul>
Related
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
I have a small problem where I can't seem to reliably render list items inside of a form using MVC4. Here is a small slug of code where the problem comes up:
...
#{int count = Model.Details.Count;}
#for (int i = 0; i < count; i++)
{
<tr class="added-item">
<td>
#Html.EditorFor(x => Model.Details[i].WidthFeet)
#Html.EditorFor(x => Model.Details[i].WidthInches)
</td>
...
Basically I am going through the loop, and adding rows as I go. I have stepped through this process manually, and I have confirmed that the correct indexes / data are being used, and I have confirmed that the HTML is correct as far as names are concerned (properly indexed, etc.) Despite these facts, the problem that I am having is that the values in all of the input boxes wind up being the same on a row by row basis. Basically, if I change a field in one, that value will be displayed in all of the other rows when the form renders again.
Can anyone tell me what is going on, or how I can fix this ?
I think you should use foreach:
#foreach (var Item in Model.Details)
{
#Html.EditorFor(x => Item.WidthFeet)
#Html.EditorFor(x => Item.WidthInches)
}
Hi Could you please try the below? It is working for me.. If it still load the same data, please have a look in to your model data whether it is pulling duplicate data.
**#for (int count = 0; count < Model.Details.Count; count++)
{
<tr class="added-item">
<td>
#Html.EditorFor(m => m.Details[count].WidthFeet)
#Html.EditorFor(m => m.Details[count].WidthInches)
</td>
</tr>
}**
Getting some weird behavior on a dropdownlistfor() but it's using a list so perhaps i got something wrong
the code is simple enough
#for (int i = 0; i < Model.Phones.Count; i++ )
{
<tr>
<td>#Html.TextBoxFor(m => m.Phones[i].Num)</td>
<td>#Html.DropDownListFor(m => m.Phones[i].Typ, list1 )</td>
</tr>
}
where list1 is defined in the .cshtml itself as
string[] types = new string[] { "Office", "Mobile", "Home" };
List<SelectListItem> list1 = new List<SelectListItem>();
foreach(var t in types){
list1.Add(new SelectListItem{Text = t, Value = t });
the problem is the correct values are not being selected in the dropdown
whereas the dropdowns should be Mobile, Office, Home
The code is pretty vanilla, it's the standard html.DropdownListFor() helper so looks like it's not generating the correct selected attribute on the tag !!
what gives ?
The problem is that all your DropDownListFor end up using the same list of options. And the list of options itself contains the selected option. So when the list is updated with the selected option for one list, all the previous ones are updated too.
Use something like this instead:
#Html.DropDownListFor(m => m.Phones[i].Typ,
new SelectList(list1, "DataValueFieldName", "TextValueFieldName",
m => m.Phones[i].Typ)
This will generate a new unique list for each drop down list. Replace "DataValueFieldName" and "TextValueFieldName" with the names of the properties of the objects in list1 containing the value and text to display respectfully. If list1 is IEnumerable<SelectListItem>, then you can use "Value" and "Text".
I have a table where each row contains fields for a form.
I iterate in the view using this:
#foreach (var item in Model) { }
Within the foreach, I create a new table row with various fields contained inside. e.g.:
<tr><td> #Html.DropDownList("granny", "") </td></tr>
"granny" is being passed via the controller, and looks a bit like this:
ViewData["granny"] = new SelectList(db.getGrannies(), "grannyid", "grannyname");
Everything's working pretty well. The view is getting its grannies, and everything looks good. I noticed however that the name property of the field { e.g....
<select id="granny" name="granny">
} is the exact same for EVERY row created. This is a problem. I want to toss this data back to the controller in the form of a FormCollection and do fun stuff with all these grannies. I can't do that if they're not all getting passed.
That is, I'd like the selects to read, instead, like this:
<select id="granny1" name="granny1">
<select id="granny2" name="granny2">
I researched the problem a bit, and tried using
new { #Name="xyz" + n }
But Visual Studio didn't like that much.
The short and sweet of it all is this:
How do I give ViewData-generated Html.DropDownLists their own unique ids/names?
How do I give ViewData-generated Html.DropDownLists their own unique ids/names?
You cannot change the name of the generated input field and this is by design. Only the id could be changed. This being said you shouldn't need to do that. You could do the following:
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.DropDownList(
"selectedGranny",
(IEnumerable<SelectListItem>)ViewData["granny"]
)
</td>
</tr>
}
It seems like you are either trying to databind a collection, or you just need to manually name your selects (as they are really different controls on the web form).
You can use the following overload to pass html parameters to the drop down :
#Html.DropDownList(
"CategoryID",
(SelectList)ViewBag.CategoryId,
"--Select One--",
new{ //anonymous type
name = "granny1",
#class = "myCssClass",
onchange = "someFunction();"
})
I'm using a model that contains a List as a property. I'm populating this list with items i grab from SQL Server. I want the List to be hidden in the view and passed to the POST action. Later on i may want to add more items to this List with jQuery which makes an array unsuitable for expansion later on. Normally you would use
#Html.HiddenFor(model => model.MyList)
to accomplish this functionality, but for some reason the List in POST is always null.
Very simple question, anyone know why MVC behaves like this?
I've just come across this issue and solved it simply by doing the following:
#for(int i = 0; i < Model.ToGroups.Count; i++)
{
#Html.HiddenFor(model => Model.ToGroups[i])
}
By using a for instead of a foreach the model binding will work correctly and pick up all of your hidden values in the list. Seems like the simplest way to solve this problem.
HiddenFor is not like a DisplayFor or EditorFor. It won't work with collections, only single values.
You can use the Serialize HTML helper available in the MVC Futures project to serialize an object to a Hidden field, or you will have to write the code yourself. A better solution is to simply serialize an ID of some sort and re-get the data from the database on postback.
It's a bit of a hack, but if #Html.EditorFor or #Html.DisplayFor work for your list, if you want to make sure it's sent on the post request but not visible, you could just style it to using display: none; to hide it instead, e.g:
<div style="display: none;">#Html.EditorFor(model => model.MyList)</div>
What about using Newtonsoft to deserialize the object into a json string and then insert that into your Hidden field e.g.
(Model.DataResponse.Entity.Commission is a List of simple "CommissionRange" objects as you'll see in the JSON)
#using (Ajax.BeginForm("Settings", "AffiliateProgram", Model.DataResponse, new AjaxOptions { UpdateTargetId = "result" }))
{
string commissionJson = JsonConvert.SerializeObject(Model.DataResponse.Entity.Commission);
#Html.HiddenFor(data => data.DataResponse.Entity.Guid)
#Html.Hidden("DataResponse_Entity_Commission", commissionJson)
[Rest of my form]
}
Renders as:
<input id="DataResponse_Entity_Commission" name="DataResponse_Entity_Commission" type="hidden" value="[{"RangeStart":0,"RangeEnd":0,"CommissionPercent":2.00000},{"RangeStart":1,"RangeEnd":2,"CommissionPercent":3.00000},{"RangeStart":2,"RangeEnd":0,"CommissionPercent":2.00000},{"RangeStart":3,"RangeEnd":2,"CommissionPercent":1.00000},{"RangeStart":15,"RangeEnd":10,"CommissionPercent":5.00000}]">
In my case I do some JS stuff to edit the json in the hidden field before posting back
In my controller I then use Newtonsoft again to deserialize:
string jsonCommissionRange = Request.Form["DataResponse_Entity_Commission"];
List<CommissionRange> commissionRange = JsonConvert.DeserializeObject<List<CommissionRange>>(jsonCommissionRange);
Html.HiddenFor is designed for only one value. You will need to serialize your list in some way before creating the hidden field.
For example, if your list is of type string, you could join the list into a comma separated list, then split the list after post back in your controller.
I've just found out (after a couple of hours of trying to figure out why model values weren't going back to the controller) that hidden for should follow the EditorFor.
Unless I am doing something else wrong this is what I found. I will not make the mistake again.
In the context of a Model that contains a list of another class.
This will NOT work:
#{
for (int i = 0; i < Model.Categories.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(modelItem => Model.Categories[i].Id)
#Html.HiddenFor(modelItem => Model.Categories[i].ProductCategoryId)
#Html.HiddenFor(modelItem => Model.Categories[i].CategoryName)
#Html.DisplayFor(modelItem => Model.Categories[i].CategoryName)
</td>
<td>
#Html.HiddenFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
#Html.EditorFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
#Html.ValidationMessageFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
</td>
<td style="text-align: center">
#Html.HiddenFor(modelItem => Model.Categories[i].IsSelected)
#Html.EditorFor(modelItem => Model.Categories[i].IsSelected)
</td>
</tr>
}
}
Where as this WILL......
for (int i = 0; i < Model.Categories.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(modelItem => Model.Categories[i].Id)
#Html.HiddenFor(modelItem => Model.Categories[i].ProductCategoryId)
#Html.HiddenFor(modelItem => Model.Categories[i].CategoryName)
#Html.DisplayFor(modelItem => Model.Categories[i].CategoryName)
</td>
<td>
#Html.EditorFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
#Html.HiddenFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
#Html.ValidationMessageFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
</td>
<td style="text-align: center">
#Html.EditorFor(modelItem => Model.Categories[i].IsSelected)
#Html.HiddenFor(modelItem => Model.Categories[i].IsSelected)
</td>
</tr>
}
I started digging through the source code for HiddenFor, and I think the roadblock you're seeing is that your complex object MyList is not implicitly convertible to type string, so the framework treats your Model value as null and renders the value attribute empty.
You can take a look on this solution.
Put only HiddenFor inside the EditorTemplate.
And in your View put this: #Html.EditorFor(model => model.MyList)
It should works.
Faced the same issue. Without for loop, it only posted the first element of the list. After iterating through for loop, it can keep full list and post successfully.
#if (Model.MyList!= null)
{
for (int i = 0; i < Model.MyList.Count; i++)
{
#Html.HiddenFor(x => x.MyList[i])
}
}
Another option would be:
<input type="hidden" value=#(string.Join(",", Model.MyList)) />
The foreach loop instead of a for loop might be a slightly cleaner solution.
#foreach(var item in Model.ToGroups)
{
#Html.HiddenFor(model => item)
}
Another possible way to fix this would be to give each object in your List an ID, then use #Html.DropDownListFor(model => model.IDs) and populate an array which holds the IDs.
maybe late, but i created extension method for hidden fields from collection (with simple data type items):
So here it is:
/// <summary>
/// Returns an HTML hidden input element for each item in the object's property (collection) that is represented by the specified expression.
/// </summary>
public static IHtmlString HiddenForCollection<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) where TProperty : ICollection
{
var model = html.ViewData.Model;
var property = model != null
? expression.Compile().Invoke(model)
: default(TProperty);
var result = new StringBuilder();
if (property != null && property.Count > 0)
{
for(int i = 0; i < property.Count; i++)
{
var modelExp = expression.Parameters.First();
var propertyExp = expression.Body;
var itemExp = Expression.ArrayIndex(propertyExp, Expression.Constant(i));
var itemExpression = Expression.Lambda<Func<TModel, object>>(itemExp, modelExp);
result.AppendLine(html.HiddenFor(itemExpression).ToString());
}
}
return new MvcHtmlString(result.ToString());
}
Usage is as simple as:
#Html.HiddenForCollection(m => m.MyList)
Adding to this answer, I had a Model with various properties, some of which were IEnumerables. What I did was to serialize it in a variable in the view with:
#{
var serializedObject = JsonSerializer.Serialize(Model);
}
And then, put that string into a Hidden element (since HiddenFor is intended for a single value from a Model) like this:
#Html.Hidden("serialized", #serializedObject)
And finally, in the controller, I could deserialize it with
JsonSerializer.Deserialize<MyType>(Request.Form["serialized"])