Edit IEnumerable collection of items simultaneously in a View in MVC 5 - c#

I have a collection of items, name it type "A", that I want to view and edit some of its attributes in a View. I would like it to save simultaneously, however, this does not seem to work as it does not seem like it is passing anything back to the Post method.
My Controller:
[HttpPost]
public ActionResult inline(IEnumerable<A> listA)
{
for (int i = 0; i <= listA.Count(); i++ )
{
A theObj = listA.ElementAt(i);
db.SaveChanges();
} //Somehow this is returning to be Null
}
My View:
#model IEnumerable<A>
#using (Html.BeginForm())
{
#Html.EditorForModel("Multiple")
}
So far this prints out all the Id of the entries (with no formatting/line breaks, of course) for some reason.
And I am not sure how to create an editor template "Multiple", this is what I have so far:
#model Models.A
#Html.EditorFor(x => x.Id)
I'm not sure where to put it / create it, so I just made it as another view in the same folder.
Any pointers how I can make this to work, so that I can edit multiple entries of the same object in the same view and pass it back to the controller and save it? I'm a newbie to MVC, so if this seems like a really simple question, I apologize in advance. Thanks!

Like most things in programming, there's multiple ways to achieve this. If you want to go the editor template route, though, it's pretty straight-forward.
As #JamieD77 pointed out, editor templates go into Views\Shared\EditorTemplates. The most important part of that path is the EditorTemplates directory convention, though. Just as with any other view in MVC, you can override/fallback depending on where you place your view. For example, Areas\Foo\Views\Shared\EditorTemplates, will work as well, but then it's only available to the Foo area. Or, you can override it for a particular controller by placing it in Views\Foo\EditorTemplates.
Then, the name of the view should correspond with the type it's intended to be used with. In your case, the view should be named A.cshtml. You can technically specify the view name manually by either passing it to EditorFor or using something like UIHint, but it's easier and more foolproof to just rely on convention here.
Inside this view, you should create the look and feel you want to have a for a single instance of A, with all editable properties represented. Then, in your main view, you simply call Html.EditorFor on the collection property, which in your case here, is the whole model:
#Html.EditorFor(model => model)
Razor will realize it has a collection and render the editor template for each item in the collection. Importantly, because it has this context, it will also be able to generate the appropriate indexes on the field names.
If you did something like the following instead:
#foreach (var item in Model)
{
#Html.EditorFor(m => item)
}
Your field names would not be indexed, and the modelbinder would not know what to do with the posted data. If you wanted to use a loop, you would have to use indexing inside the loop:
#for (var i = 0; i < Model.Count(); i++)
{
#Html.EditorFor(m => Model[i])
}
That then gives Razor the proper context to generate appropriate field names. However, importantly, that approach requires utilizing a List<T> structure, rather than something like IEnumerable<T> or ICollection<T>.

Related

Create several instances of a model in one page asp.net mvc

I want to create several instances of my model:
public int RestrictionID{get;set}
public string portefeuille{get;set;}
public int Min{get;set;}
public int Max{get;set;}
So I created a ViewModel that is:
public int ViewModelID{get;set;}
public ICollection<Restriction> Restrictions{get;set;}
But when using entity framework, and trying to create an instance of ViewModel(that is several instance of Restriction) nothing appears (no input for the user), same as if the compiler don't know how to represent the input of Restrictions. So what I want is exactly the same input(properties) to be filled severeal times say 4 times.
How can I achieve this? I need some help..
Thank you!
Does the ViewModelID serve any purpose? If not, you can just type your View to an IEnumerable of Restriction objects, like so:
#model IEnumerable<Restriction>
Then, in your view, you can enumerate the collection as you would any other:
#foreach (var restriction in Model)
{
#Html.DisplayFor(_ => restriction.portefeuille)
etc....
}
However worth noting that if you want to be able to edit and post back multiple instances of your model you will need to use an indexed loop like so:
#for (int i=0; i < Model.Count; i++)
{
#Html.EditorFor(_ => Model[i].portefeuille)
etc...
}
This allows the model binder to correctly bind the values.
If you do need the ViewModelId, then by all means use the ViewModel (it's good practice to use them anyway), and the same techniques still apply.
Your question is a little hard to understand, but I think your problem is merely that you have not initialized your Restrictions. For example:
model.Restrictions = new List<Restriction>
{
new Restriction(),
new Restriction(),
new Restriction(),
new Restriction()
}
Would give you four sets of Restriction fields, once you rendered this collection in your view. You might also what to do something like the following instead:
for (var i = 0; i < 4; i++)
{
model.Restrictions.Add(new Restriction);
}
That's a little less verbose, particularly if you were to have more than 4 items. Either way, the idea is that you need an actual Restriction instance in your collection for each set of fields you want rendered on the page.
If you need to add more dynamically, that's an entirely different issue that will require the use of JavaScript.

Post item of IEnumaration to controller

I am having trouble figuring out what is the best aproach to a rather simple problem.
I have a payment view-model that contains a list of avalible payments and some base properties like title, content...
So in my view I use #using(Html.BeginForm()) and inside that I loop over the payments and render each out and of course the view-model that I recive in my controller post has and empty list of payment methods.
I can see that if I use a for and print out model.paymentMethods[i].Prop than it can map it but is this the right aproach or can I do something even smarter?
For the model-binder to work you need to use indexed access.
#foreach (var item in Model.PaymentMethods)
{
#Html.EditorFor(m => item.Prop);
}
does NOT work.
You need to use:
#for (int i = 0; i < m.PaymentMethods.Count(), ++i)
{
#Html.EditorFor(m => m.PaymentMethods[i].Prop);
}
Otherwise the model-binder can't map it back in the post-back.
Yoy can create a editor template of Payment type. And in view you can use that editor template like:
#Html.EditorFor(m => m.PaymentMethods)

How do you put a ViewBag item into a Text Box?

I have the following code and I get an error saying:
has no applicable method named 'TextBoxFor' but appears to have an extension method by that name.
My Code:
#Html.TextBoxFor(ViewBag.taglist)
Why don't you use strongly typed model in your view instead of ViewBag. This will make your life easier.
In fact, you must use a model to with TextBoxFor, otherwise it just won't work. See the definition of TextBoxFor - as a second parameter it takes a lambda expression that takes a property form a model.
If you want just a text box, two options:
#Html.TextBox("NameOfTheTextbox", (String)ViewBag.SomeValue)
or just go
<input type="text" value="#ViewBag.SomeValue" />
No complex solutions required.
I agree with other suggestions of using a strongly-typed model, because the compile-time error support is so much better than debugging exceptions. Having said that, in order to do what you want, you can use this:
#Html.TextBox("NameOfTextBox", (string)ViewBag.taglist)
Update: A Simple Example
Now that you've provided some details in your comments, I've taken a guess at what you might be doing, in order to provide a simple example.
I'm assuming you have a list of tags (like SO has per question) that you'd like to display neatly in a textbox, with each tag separated by a space. I'm going to assume your Tag domain model looks something like this:
public class Tag
{
public int Id { get; set; }
public string Description { get; set; }
}
Now, your view will need a list of the tags but will likely need some other information to be displayed as well. However, let's just focus on the tags. Below is a view model to represent all the tags, taking into account that you want to display them as a string inside a textbox:
public class SomeViewModel
{
public string Tags { get; set; }
// Other properties
}
In order to get the data you want you could grab all of the tags like this:
public ActionResult Index()
{
using (YourContext db = new YourContext())
{
var model = new SomeViewModel();
model.Tags = string.Join(" ", db.Tags.Select(t => t.Description).ToList());
return View(model);
}
}
Notice how I'm directly passing model to the view.
The view is now very simple:
#model SomeViewModel
#Html.EditorFor(m => m.Tags)
The model directive is what signifies that a view is strongly-typed. That means this view will expect to receive an instance of SomeViewModel. As you can see from my action code above, we will be providing this view the type that it wants. This now allows us to make use of the strongly-typed HtmlHelper (i.e. Html.XxxFor) methods.
In this particular case, I've used Html.EditorFor, as it will choose an appropriate input element to render the data with. (In this case, because Description is a string, it will render a textbox.)
You cannot use Html.TextBoxFor without explicitly setting a type for your model within the view. If you don't specify a type it defaults to dynamic. If you want to do model binding then you must use an explicit type rather than a dynamic type like ViewBag. To use Html.TextBoxFor you must define a model type that defines the property that you wish to bind. Otherwise you have to use Html.TextBox and set the value manually from ViewBag. As others have said, you will make your life much easier if you use a statically typed model and take advantage of the inbuilt MVC model binding.
You have to use a lambda expression to select the property, plus you will have to cast the ViewBag member to the correct type.
#Html.TextBoxFor(model => (string)ViewBag.taglist)

Collection data not appearing in models after being posted. (Includes collections of collections.)

I have a PersonEditorModel that contains a list of people that I want represented in the editor. I have a collection of Person objects that contains a collection of Address objects. I want to render text boxes for all of these so that the user can edit names and address associated with those names.
class PersonEditorModel
{
public List<Person> People;
}
class Person
{
public string Name;
public List<Address> Addresses;
}
class Address
{
public string Value;
}
I'm generating a form for editing the addresses and the names associated with them. It's using Ajax.BeginForm because this is a simplified example derived from a problem I'm having in a bigger app where the form updates a different part of the page using Ajax to get a result.
In the base form's cshtml:
#model Models.PersonEditorModel
#using (Ajax.BeginForm("Update", new AjaxOptions { HttpMethod = "Post" }))
{
<div>
#Html.EditorFor(x => x.People)
<button type="submit">Commit Changes</button>
</div>
}
Person.cshtml:
#model Models.Person
<div> #Html.TextBoxFor(x => x.Name) </div>
<div> #Html.EditorFor(x => x.Addresses) </div>
Address.cshtml:
#model Models.Address
<div> #Html.TextBoxFor(x => x.Value) </div>
The controller's method for the action:
public ActionResult Update(List<Person> people) { /* snip */ }
When I submit this form, a breakpoint placed immediately within Update() shows that "people" is a list of the right length but it contains absolutely no data--all the addresses are empty strings, even though the form data as appears in Request.Form looks correct.
What could be causing such a problem and what would be an idiomatic MVC way to solve this issue?
NOTE: This is a simplified example of some behavior I'm seeing in a more complex app. I think I've captured the essence of the issue here, but there may be some other unknown complicating factor. Let me know what could complicate this to cause the issue I'm seeing and I'll try to provide whatever additional details I can.
I've tried making Update take a PersonEditorModel parameter instead, but that gets absolutely no data at all.
class Address
{
public string Value;
}
should be:
class Address
{
public string Value { get; set; }
}
The default model binder works with properties, not fields.
Also make sure the generated input field names respect the standard convention.
It seems to me that you have at least two problems here, both model-related. First is that the object being posted to your call is built automagically based upon the ids of input elements in the generated form. For example to extract a property called PersonName, there would need to be a tag somewhere resembling
<input type='whatever' id='PersonName' />
As such, these properties must be able to be represented via such an input.
The second problem stems from this first. Using MVC's built in ajax forms (which are limited compared to AJAX in general (and in jQuery)), there is no way (that I know of, at least) to define any collections, but especially not something as generic as a list. To do this using MVC's AJAX, you would probably need to implement an editor for one person/user at a time and submit that data (with a singular or enumerated number of addresses) passed back.
Also, you might want to reconsider updating a whole set of people via one AJAX call. The dynamic nature of AJAX better lends itself to smaller, more frequent updates.

MVC Razor view nested foreach's model

Imagine a common scenario, this is a simpler version of what I'm coming across. I actually have a couple of layers of further nesting on mine....
But this is the scenario
Theme contains List
Category contains List
Product contains List
My Controller provides a fully populated Theme, with all the Categories for that theme, the Products within this categories and the their orders.
The orders collection has a property called Quantity (amongst many others) that needs to be editable.
#model ViewModels.MyViewModels.Theme
#Html.LabelFor(Model.Theme.name)
#foreach (var category in Model.Theme)
{
#Html.LabelFor(category.name)
#foreach(var product in theme.Products)
{
#Html.LabelFor(product.name)
#foreach(var order in product.Orders)
{
#Html.TextBoxFor(order.Quantity)
#Html.TextAreaFor(order.Note)
#Html.EditorFor(order.DateRequestedDeliveryFor)
}
}
}
If I use lambda instead then then I only seem to get a reference to the top Model object, "Theme" not those within the foreach loop.
Is what I'm trying to do there even possible or have I overestimated or misunderstood what is possible?
With the above I get an error on the TextboxFor, EditorFor, etc
CS0411: The type arguments for method
'System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper,
System.Linq.Expressions.Expression>)'
cannot be inferred from the usage. Try specifying the type arguments
explicitly.
Thanks.
The quick answer is to use a for() loop in place of your foreach() loops. Something like:
#for(var themeIndex = 0; themeIndex < Model.Theme.Count(); themeIndex++)
{
#Html.LabelFor(model => model.Theme[themeIndex])
#for(var productIndex=0; productIndex < Model.Theme[themeIndex].Products.Count(); productIndex++)
{
#Html.LabelFor(model=>model.Theme[themeIndex].Products[productIndex].name)
#for(var orderIndex=0; orderIndex < Model.Theme[themeIndex].Products[productIndex].Orders; orderIndex++)
{
#Html.TextBoxFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Quantity)
#Html.TextAreaFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Note)
#Html.EditorFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].DateRequestedDeliveryFor)
}
}
}
But this glosses over why this fixes the problem.
There are three things that you have at least a cursory understanding before you can resolve this issue. I have
to admit that I cargo-culted this
for a long time when I started working with the framework. And it took me quite a while
to really get what was going on.
Those three things are:
How do the LabelFor and other ...For helpers work in MVC?
What is an Expression Tree?
How does the Model Binder work?
All three of these concepts link together to get an answer.
How do the LabelFor and other ...For helpers work in MVC?
So, you've used the HtmlHelper<T> extensions for LabelFor and TextBoxFor and others, and
you probably noticed that when you invoke them, you pass them a lambda and it magically generates
some html. But how?
So the first thing to notice is the signature for these helpers. Lets look at the simplest overload for
TextBoxFor
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
First, this is an extension method for a strongly typed HtmlHelper, of type <TModel>. So, to simply
state what happens behind the scenes, when razor renders this view it generates a class.
Inside of this class is an instance of HtmlHelper<TModel> (as the property Html, which is why you can use #Html...),
where TModel is the type defined in your #model statement. So in your case, when you are looking at this view TModel
will always be of the type ViewModels.MyViewModels.Theme.
Now, the next argument is a bit tricky. So lets look at an invocation
#Html.TextBoxFor(model=>model.SomeProperty);
It looks like we have a little lambda, And if one were to guess the signature, one might think that the type for
this argument would simply be a Func<TModel, TProperty>, where TModel is the type of the view model and TProperty
is inferred as the type of the property.
But thats not quite right, if you look at the actual type of the argument its Expression<Func<TModel, TProperty>>.
So when you normally generate a lambda, the compiler takes the lambda and compiles it down into MSIL, just like any other
function (which is why you can use delegates, method groups, and lambdas more or less interchangeably, because they are just
code references.)
However, when the compiler sees that the type is an Expression<>, it doesn't immediately compile the lambda down to MSIL, instead it generates an
Expression Tree!
What is an Expression Tree?
So, what the heck is an expression tree. Well, it's not complicated but its not a walk in the park either. To quote ms:
| Expression trees represent code in a tree-like data structure, where each node is an expression, for example, a method call or a binary operation such as x < y.
Simply put, an expression tree is a representation of a function as a collection of "actions".
In the case of model=>model.SomeProperty, the expression tree would have a node in it that says: "Get 'Some Property' from a 'model'"
This expression tree can be compiled into a function that can be invoked, but as long as it's an expression tree, it's just a collection of nodes.
So what is that good for?
So Func<> or Action<>, once you have them, they are pretty much atomic. All you can really do is Invoke() them, aka tell them to
do the work they are supposed to do.
Expression<Func<>> on the other hand, represents a collection of actions, which can be appended, manipulated, visited, or compiled and invoked.
So why are you telling me all this?
So with that understanding of what an Expression<> is, we can go back to Html.TextBoxFor. When it renders a textbox, it needs
to generate a few things about the property that you are giving it. Things like attributes on the property for validation, and specifically
in this case it needs to figure out what to name the <input> tag.
It does this by "walking" the expression tree and building a name. So for an expression like model=>model.SomeProperty, it walks the expression
gathering the properties that you are asking for and builds <input name='SomeProperty'>.
For a more complicated example, like model=>model.Foo.Bar.Baz.FooBar, it might generate <input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />
Make sense? It is not just the work that the Func<> does, but how it does its work is important here.
(Note other frameworks like LINQ to SQL do similar things by walking an expression tree and building a different grammar, that this case a SQL query)
How does the Model Binder work?
So once you get that, we have to briefly talk about the model binder. When the form gets posted, it's simply like a flat
Dictionary<string, string>, we have lost the hierarchical structure our nested view model may have had. It's the
model binder's job to take this key-value pair combo and attempt to rehydrate an object with some properties. How does it do
this? You guessed it, by using the "key" or name of the input that got posted.
So if the form post looks like
Foo.Bar.Baz.FooBar = Hello
And you are posting to a model called SomeViewModel, then it does the reverse of what the helper did in the first place. It looks for
a property called "Foo". Then it looks for a property called "Bar" off of "Foo", then it looks for "Baz"... and so on...
Finally it tries to parse the value into the type of "FooBar" and assign it to "FooBar".
PHEW!!!
And voila, you have your model. The instance the Model Binder just constructed gets handed into requested Action.
So your solution doesn't work because the Html.[Type]For() helpers need an expression. And you are just giving them a value. It has no idea
what the context is for that value, and it doesn't know what to do with it.
Now some people suggested using partials to render. Now this in theory will work, but probably not the way that you expect. When you render a partial, you are changing the type of TModel, because you are in a different view context. This means that you can describe
your property with a shorter expression. It also means when the helper generates the name for your expression, it will be shallow. It
will only generate based on the expression it's given (not the entire context).
So lets say you had a partial that just rendered "Baz" (from our example before). Inside that partial you could just say:
#Html.TextBoxFor(model=>model.FooBar)
Rather than
#Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)
That means that it will generate an input tag like this:
<input name="FooBar" />
Which, if you are posting this form to an action that is expecting a large deeply nested ViewModel, then it will try to hydrate a property
called FooBar off of TModel. Which at best isn't there, and at worst is something else entirely. If you were posting to a specific action that was accepting a Baz, rather than the root model, then this would work great! In fact, partials are a good way to change your view context, for example if you had a page with multiple forms that all post to different actions, then rendering a partial for each one would be a great idea.
Now once you get all of this, you can start to do really interesting things with Expression<>, by programatically extending them and doing
other neat things with them. I won't get into any of that. But, hopefully, this will
give you a better understanding of what is going on behind the scenes and why things are acting the way that they are.
You can simply use EditorTemplates to do that, you need to create a directory named "EditorTemplates" in your controller's view folder and place a seperate view for each of your nested entities (named as entity class name)
Main view :
#model ViewModels.MyViewModels.Theme
#Html.LabelFor(Model.Theme.name)
#Html.EditorFor(Model.Theme.Categories)
Category view (/MyController/EditorTemplates/Category.cshtml) :
#model ViewModels.MyViewModels.Category
#Html.LabelFor(Model.Name)
#Html.EditorFor(Model.Products)
Product view (/MyController/EditorTemplates/Product.cshtml) :
#model ViewModels.MyViewModels.Product
#Html.LabelFor(Model.Name)
#Html.EditorFor(Model.Orders)
and so on
this way Html.EditorFor helper will generate element's names in an ordered manner and therefore you won't have any further problem for retrieving the posted Theme entity as a whole
You could add a Category partial and a Product partial, each would take a smaller part of the main model as it's own model, i.e. Category's model type might be an IEnumerable, you would pass in Model.Theme to it. The Product's partial might be an IEnumerable that you pass Model.Products into (from within the Category partial).
I'm not sure if that would be the right way forward, but would be interested in knowing.
EDIT
Since posting this answer, I've used EditorTemplates and find this the easiest way to handle repeating input groups or items. It handles all your validation message problems and form submission/model binding woes automatically.
When you are using foreach loop within view for binded model ...
Your model is supposed to be in listed format.
i.e
#model IEnumerable<ViewModels.MyViewModels>
#{
if (Model.Count() > 0)
{
#Html.DisplayFor(modelItem => Model.Theme.FirstOrDefault().name)
#foreach (var theme in Model.Theme)
{
#Html.DisplayFor(modelItem => theme.name)
#foreach(var product in theme.Products)
{
#Html.DisplayFor(modelItem => product.name)
#foreach(var order in product.Orders)
{
#Html.TextBoxFor(modelItem => order.Quantity)
#Html.TextAreaFor(modelItem => order.Note)
#Html.EditorFor(modelItem => order.DateRequestedDeliveryFor)
}
}
}
}else{
<span>No Theam avaiable</span>
}
}
It is clear from the error.
The HtmlHelpers appended with "For" expects lambda expression as a parameter.
If you are passing the value directly, better use Normal one.
e.g.
Instead of TextboxFor(....) use Textbox()
syntax for TextboxFor will be like Html.TextBoxFor(m=>m.Property)
In your scenario you can use basic for loop, as it will give you index to use.
#for(int i=0;i<Model.Theme.Count;i++)
{
#Html.LabelFor(m=>m.Theme[i].name)
#for(int j=0;j<Model.Theme[i].Products.Count;j++) )
{
#Html.LabelFor(m=>m.Theme[i].Products[j].name)
#for(int k=0;k<Model.Theme[i].Products[j].Orders.Count;k++)
{
#Html.TextBoxFor(m=>Model.Theme[i].Products[j].Orders[k].Quantity)
#Html.TextAreaFor(m=>Model.Theme[i].Products[j].Orders[k].Note)
#Html.EditorFor(m=>Model.Theme[i].Products[j].Orders[k].DateRequestedDeliveryFor)
}
}
}
Another much simpler possibility is that one of your property names is wrong (probably one you just changed in the class). This is what it was for me in RazorPages .NET Core 3.

Categories