I am fairly new to MVC, but have quite a bit of experience in development in general, and am having an issue with MVC request life cycle it seems.
Will try to keep this simple, even tho the project is a bit complex in some areas.
I have a view bound to a view model that has a few complex list properties. These properties are displayed via checkboxes who's IDs are not directly related to any property in the model, but instead related to the IDs of the objects in the List<>. Because of this, the checked values do not automatically get applied to the model on POST.
To get around that, I added code in the Action method in the controller that parses the proper controls (in the Request.Form collection) and assigns the checked/selected value to the proper list items in the model.
This works perfectly up to a point.
Now, I also use Fluent Validation, and the problem is when performing custom validation rules when posting a new model to the server. The Validation routine is firing BEFORE the controller's action method, and thus before my processing of the list objects.
So, my question is, is there a way I can override the initial call to the model validation so I can just call the validation manually after my processing? I know I can do that which will fix the problem without overriding the initial call, but some of the validation takes a bit of time to process since it requires linq queries to a live database, so I do not want the validation to fire 2 times - that will quite literally double the time it takes to return no matter if the model is valid or not.
EDIT: Adding a example:
namespace Models
{
[Validator(typeof(MemberValidator))]
public class ViewMember
{
public int MemberID { get; set; }
public short RegionID { get; set; }
public List<PropTypeInfo> PropTypes { get; set; }
}
}
PropTypeInfo class:
public class PropTypeInfo
{
public byte ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool Selected { get; set; }
public PropTypeInfo(byte i, string n, string d, bool sel)
{
ID = i;
Name = n;
Description = d;
Selected = sel;
}
public static List<PropTypeInfo> GetAll(bool selected = false)
{
List<PropTypeInfo> output = new List<PropTypeInfo>();
OpenAccess.Context context = new OpenAccess.Context();
var list = (from f in context.Prop_Types orderby f.PropType select f).ToList();
foreach (OpenAccess.WebrentzServerPayments.Models.Prop_Type p in list)
output.Add(new PropTypeInfo(p.PropType, p.PropName, p.DisplayText, selected));
return output;
}
}
now here is the code in the view that renders the checkboxes for each item in the list:
<div class="Column Emp-PropTypes">
#foreach (WebrentzServerPayments.Models.PropTypeInfo ptype in Model.PropTypes)
{
<div style="float:right;width:20%;font-weight:bold;">
#Html.CheckBox("ptype_" + ptype.ID, ptype.Selected, new {Value=ptype.ID}) #Html.Raw(" ") #ptype.Name
</div>
}
</div>
And here is the code I use in the Controller Action method to pull that data back in to the List:
foreach (PropTypeInfo info in member.PropTypes)
info.Selected = form[string.Format("ptype_{0}", info.ID)].Contains(info.ID.ToString());
As a little background, a "PropType" is a type of property (house, condo, apartment) - there are about 2 dozen of them, and more can be added/removed at any time. The list in the class called "PropTypes" is first populated with the Name, Description and ID from a table in the database that lists all the available proptypes for that region.
We then will mark the proptypes as "selected" if the user has chosen that particular type. Those are saved to a table called Member.PropTypes (MemberID, ProptypeID).
So, at runtime the list will contain one record for each available proptype and the selected property will be set to yes if that user has selected it. That makes it easy to render the full list in the view...
Its actually quite a bit more complex as there are almost a dozen such lists, but each works the exact same way just with different data, as well as about 200 additional properties that are easier to manage. Only these lists are causing the issue.
Any help appreciated!
Dave
Related
I'm building a simple web portal using the .Net Core 7 MVC template in VS2022 (It's challenging to know if this is the latest recommended format and or template, Microsoft doesn't really state what is the current approach from a stack perspective).
As part of building out and focusing on MVC I looked at if PageModels were the recommended method of binding to the page or if using a simple Model and populating the page using razor syntax (Which is my preference) was the most modern recommendation (Again Microsoft documentation isn't exactly specific).
That aside, the specific problem that I have is that I'm building out a settings page that has a number of different forms and I initially thought that in order to maintain the page state of other inputs and fields that when a form posts to the controller it sends the entire page model with the form bound properties populated, perform some logic, and pass a modified model back the the main Index ActionMethod. However I can't seem to see anything specific around sending the entire model back that was used by the razor page as part of individual form submits. Or moreover, if it's even a good idea.
The page structure looks like this.
Form - Team Name
Button - Submit, just saves the team name.
List - Team Members currently in team.
Form - Search for Users by Email.
List - List of Users matching Search input, with button to Add To Team
The issue i have is that if I pass back the form properties which are bound to the model from the search input, i don't get all of the other model properties used for populating current team members list, or the team name, so I assume that would mean that the controller has to do the work of getting that data again? This seems a bit inefficient in my mind. I'm assuming there is a clever way of doing this and achieving an appropriate outcome but I'm struggling to see the light through the dark with this particular consideration. Any suggestions or nods in the right direction are appreciated.
Example Model
public class SettingsPageModel
{
public SettingsPageModel() { }
public SettingsPageModel(string teamName)
{
TeamName = teamName;
}
public SettingsPageModel(SearchModel searchModel)
{
SearchModel = searchModel;
}
public SettingsPageModel(string teamName, List<TeamMember> teamMembers, SearchModel searchModel)
{
TeamName = teamName ?? throw new ArgumentNullException(nameof(teamName));
TeamMembers = teamMembers ?? throw new ArgumentNullException(nameof(teamMembers));
SearchModel = searchModel ?? throw new ArgumentNullException(nameof(searchModel));
}
public string TeamName { get; set; } = string.Empty;
public List<TeamMember> TeamMembers { get; set; } = new();
public SearchModel SearchModel { get; set; } = new();
public DialogModel? DialogModel { get; set; }
}
public class SearchModel
{
public string? SearchTerm { get; set; }
public List<User>? SearchedUsers { get; set; }
}
Every time you submit a form you send back ONLY the input elements on that form (not the entire model).
If you have a search input and want to update only a portion of the page you would have to either:
Add more hidden input fields to that form to send for example TeamName and populate the model completely (not ideal)
Use Ajax when the form is submitted, have an Action in your controller that gets the filtered users and return a PartialView() with the list of users, and update the DOM using JavaScript when you get your ajax response (HTML)
I am learning asp.net mvc , using visual studio community 2017, and as a sort of teaching project I am making a web app that keeps track of exercise work outs. My model consists of WorkOut objects that have a list (or ICollection more specifically) of Exercise, and each Exercise has an ICollection. Heres the basics of my model classes.
public class WorkOut
{
public int Id { get; set; }
public int Length { get; set; }
public DateTime Date { get; set; }
public virtual ICollection<Exercise> ExerciseList { get; set; }
}
public class Exercise
{
public int Id { get; set; }
public string Name { get; set; }
public int WorkOutId { get; set; }
public virtual WorkOut WorkOut { get; set; }
public virtual ICollection<RepUnit> Sets { get; set; }
}
public class RepUnit
{
public int Id { get; set; }
public int Rep { get; set; }
public int? Weight { get; set; }
public int ExerciseId { get; set; }
public virtual Exercise Exercise { get; set; }
}
Generating a view automatically with WorkOut as a model leads to Create action and view that only generates a Length and Date property. In general, auto generated view and controllers only add the non virtual properties. So I figure maybe I have to do a multistep creation process; Create a workout, create an exercise and add reps to it, add that exercise to the work out, either stop or add another exercise. So I figured Id let VS to some of the work for me, and I make controllers and views for each of the model object typers (WorkOutsController, ExercisesController, RepUnitsController), and later I would trim out the uneeded views or even refactor the actions i actually use into a new controller.
So WorkOutsController my POST action is this.
public ActionResult Create([Bind(Include = "Id,Length,Date")] WorkOut workOut)
{
if (ModelState.IsValid)
{
db.WorkOuts.Add(workOut);
db.SaveChanges();
return RedirectToAction("Create","Exercises",new { workoutId = workOut.Id });
}
return View(workOut);
}
So I carry the workoutId to the Exercise controller but this is where I am unsure how to proceed. I want to keep carrying around the workoutId and for the next step, where I give the exercise a name, also show the associated date that was just added. The only thing I could think to do was instantiate an Exercise in the GET action of ExerciseController like so.
public ActionResult Create(int workoutID)
{
Exercise ex= new Exercise();
ex.WorkOutId=workoutID;
ex.WorkOut=db.WorkOuts(workoutID);
return View(ex);
}
This seems terrible and I've not seen anything like this done in any examples, but it seems to work. The same exercise object is brought back to my POST create action here
public ActionResult Create([Bind(Include = "Id,Name,WorkOutId")] Exercise exercise)
{
if (ModelState.IsValid)
{
db.Exercises.Add(exercise);
db.SaveChanges();
return RedirectToAction("Create", "RepUnits", new { exerciseId = exercise.Id });
}
return View(exercise);
}
which as you see calls the RepUnits controller and associated Create action. There I do something very similar; create a rep object and pass it to the view, and essentially I add reps until I'm done. Eventually I will create navigation to either go back to add a new exercise or go back to an Index view.
So to sum up, it seems wasteful to be passing entire objects around, and maybe my whole implementation is wrong and I should be trying to somehow do this all on one form. Up to this point googling hasnt found me much because I wasnt sure what questions to be asking, however this post Creation of objects using form data within an ASP.NET MVC application just popped up in the similar question dialogue and the app in question is coincidentally very similar! However when the OP mentions passing the workoutId around, how is this accomplised? I thought to maybe use the ViewBag but how do I get the view to handle this Id?
I had though to try, as an example
public ActionResult Create(int workoutId)
{
ViewBag.WoID = workoutId;
return View();
}
in the ExercisesController and then in the associated Create view:
#Html.Hidden("WorkOutId", new { ViewBag.WoID })
But later in the view when I try to reference the workout date it comes up blank
<div class="form-group">
#Html.LabelFor(model => model.WorkOut.Date, "Work Out On:", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DisplayFor(model=>model.WorkOut.Date)
</div>
</div>
Should I be doing something like this in the view:
#Model.WorkOutId=ViewBag.WoID;
which doesnt work for some reason (Compiler Error Message: CS1525: Invalid expression term '='), but is that along the lines of how I pass these ids around?
The scaffolded views are intentionally simplistic. Dealing with related items requires multiple considerations, and Visual Studio won't make those for you. However, you can and are very encouraged to alter the scaffolded views to your particular needs.
To create exercises in the same view as your workout for example, you need only generate fields for Exercise with names that will allow the modelbinder to bind the posted data. For collection properties that means something like CollectionProperty[N].Property, where N is an index.
For example, you can initialize your workout with three exercises:
var model = new Workout
{
ExerciseList = new List<Exercise>
{
new Exercise(),
new Exercise(),
new Exercise()
}
};
Then, in your view:
#for (var i = 0; i < Model.Exercises.Count(); i++)
{
#Html.EditorFor(m => m.ExerciseList[i].Name)
}
However, there's one thing to note here: ICollection is not indexable. As a result, to do it this way, you'd need a property typed as List<Exercise>. This is where view models come in very handy. Nevertheless, there is a way around this: you can use EditorFor on the Excercises collection instead. For example, the above code would be reduced to just:
#Html.EditorFor(m => m.ExerciseList)
EditorFor is a "templated helper", which means simply that it uses templates to render what's passed to it. Thankfully, it has some defaults, so to a point, you don't need to worry about that, but it become problematic. For example, here, Razor will simply iterate over the items in ExerciseList and render the template for Exercise for each. Since Exercise is a custom type and doesn't have a default template, it will then introspect the class and render a template for each public property on Exercise. Sometimes this works just fine. For example, Name will be rendered with a text box, as it should be. However, you'll also get text boxes for Id and WorkoutId, which you wouldn't want to even be visible on your form.
You can solve this issue by creating your own editor template for Exercise, by adding a view to Views\Shared\EditorTemplates\Exercise.cshtml. This view would have a model of Exercise, then, and would simply include a text box for your name property. Then, when you use EditorFor on ExerciseList as above, it will render each Exercise utilizing that view.
With all that out of the way, though, you've likely realized that this is still somewhat limiting: you have to initialize with a certain number of exercises and then that's all you get. That's where JavaScript comes in. Instead of iterating over a predefined list of exercises, you can simply dynamically add a new block of exercise fields as needed (or remove existing blocks). However, writing JavaScript for this task manually would be very painstaking and dense. At this point, you're better off utilizing something like Knockout.js, Angular, or similar. These libraries, among other things, give you two-way databinding, so you could simply set up a JavaScript template for what a block of exercise fields will look like, and then bind that to an ExceriseList member of a JavaScript object (your client-side "view model"). You could then cause these fields to repeat simply by adding or removing items from this JS array. Obviously, there's much more that goes into this, but that's the basic framework. You'd need to consult the individual documentation of the library you went with to determine exactly how to set everything up.
You can then rinse and repeat all this for other levels of relationships as well. In other words, it's entirely possible to post this entire object graph of a workout with multiple exercises, each with multiple rep units, etc. all in one go with one view.
I am currently typing this from my phone, so I don't have any code at hand. But I'll try to explain the situation.
Taking the example of a Recipe and Variations to this Recipe. The Recipe in this case holds a collection of Variations.
I have a Create view that takes the Recipe as the model.
I want this view to contain a list of Variations to the Recipe already. So there is an 'Add variation' button that opens a dialog with a partial view to add this variation.
Given that this is a yet non existing Recipe, I can't save the variation to the database yet, because it requires the RecipeId. So I think I am looking for a temporary place to store the new variations until I am saving the Recipe, and then also save the variations with it.
As MVC is stateless, I can't really save the variations anywhere temporarily, except for possibly the viewdata or tempdata, or I could add a bunch of hidden fields to the page via JavaScript, so that the Variation entries will be taken into the post triggered by saving the Recipe. All of which don't sound particularly tidy solutions.
What would be the most clean, ideal solution here?
A couple of possible solutions:
Reconsider to use a wizard flow for creation instead - create recipe screen (no variations), then a chain of create variation screens. After that when you edit you can do it in the same page since the recipe is saved already.
As dreza mentionned, the variation partial view could return html to the creation page that adds the fields needed in the form for a full submit of the recipe + variations.
Pre-generate the RecipeId on the create form and reference to it for every actions, so you could save Variation (Id, RecipeId, ...) in the database before saving Recipe if you have different table/document. Then you lose the autogenerate id from SQL/Entity tho, and it's harder to maintain.
If you'd use a document database, you could have had the Recipe document have a list of VariationId and create the Variation on it's own without any direct reference to Recipe directly (No RecipeId in Variation). In SQL/Entity tho you would need 3 tables - Recipe (RecipeId), Variation (VariationId), RecipeVariation (RecipeId,VariationId) - it's uglier.
However, with complex, error prone UI like a parent + childs and such, another approach, and one I used extensively, is to create InputContexts associated to users and have InputModels are well as your normal models. These are semi-temporary models that exists until the user finishes to do his entry, they are usually used as ViewModels directly. For example, you could have:
public class InputContext
{
public int Id { get; set; }
public int UserId { get; set; }
public string RouteName { get; set; }
public DateTime TimeStamp { get; set; }
//...
}
public class RecipeInput
{
public int Id { get; set; }
public int ContextId { get; set; }
// ...
}
public class VariationInput
{
public int Id { get; set; }
public int ContextId { get; set; }
// ...
}
Your partial view that creates variation could simply save them to VariationInput with the current ContextId and that's it.
Then in the controller:
[HttpGet]
public ActionResult Create()
{
// We should check if one exists and reuse it instead, but omitted for clarity
var context = new InputContext(GetUserId(), GetRouteName());
db.Contexts.Add(context);
db.Save(context);
var model = new RecipeInput(context.Id);
return View(model);
}
[HttpPost]
public ActionResult Create(RecipeInput model)
{
// Save the current input to DB
db.RecipeInputs.Update(model);
db.Save();
// Do validation and return Create view on error...
// load the context and related variations created
var context = db.Contexts.Find(model.ContextId);
var children = db.VariationInputs.Where(x => x.ContextId == context.Id).ToList();
// Create the actual models from the input.
var recipe = new Recipe();
// set values from model
foreach (var child in children )
{
var variation = new Variation();
// set values from child
recipe.Variations.Add(variation);
}
db.Recipes.Add(recipe);
db.Save();
// Cleanup if it worked
db.RecipeInputs.Delete(model.Id);
foreach (var child in children )
{
db.VariationInputs.Delete(child.Id);
}
// you could keep the Contexts as logs or delete them
//...
}
This is quite more complicated, but it can be used to save user data even between browser restart, session timeouts (they leave for lunch and come back and have to input everything again...), etc, and do some other neat tricks.
I usually store them in another database/partition/schema, and when I use SQL as a backend (because that's what the client wants), I often try use a Document Database (RavenDB or MongoDB for example) to store the context and input models as it is much nicer to play with in terms of MVC. This is not client data so it doesn't matter much.
If you need to see the list of variations in the Create Recipe page, you simply need to do have a partial view/ajax that gets a list of VariationInput in the current ContextId and display these.
When I am changing the "model => model.id" to "model => model.Supplierid" i am getting below error
"The parameter 'expression' must evaluate to an IEnumerable when
multiple selection is allowed."
please have look on below code
// this my model class
public class clslistbox{
public int id { get; set; }
public int Supplierid { get; set; }
public List<SuppDocuments> lstDocImgs { get; set; }
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
public List<SuppDocuments> listDocImages()
{
List<SuppDocuments> _lst = new List<SuppDocuments>();
SuppDocuments _supp = new SuppDocuments();
_supp.Title = "title";
_supp.documentid = 1;
_lst.Add(_supp);
return _lst;
}
}
// this my controller
[HttpGet]
public ActionResult AddEditSupplier(int id)
{
clslistbox _lst = new clslistbox();
_lst.lstDocImgs= _lst.listDocImages();
return View(_lst);
}
// this is view where i am binding listboxfor
#model clslistbox
#using (Html.BeginForm("AddEditSupplier", "Admin", FormMethod.Post))
{
#Html.ListBoxFor(model => model.id, new SelectList(Model.lstDocImgs, "documentid", "title"))
}
Can anyone see the reason for it?
I think the changing of the property in the expression here is a red-herring - it won't work in either case.
Update
However, see at the end of my answer for some probably needlessly detailed exposition on why you didn't get an error first-time round.
End Update
You're using ListBoxFor - which is used to provide users with multiple selection capabilities - but you're trying to bind that to an int property - which cannot support multiple selection. (It needs to be an IEnumerable<T> at least to be able to bind a list box to it by default in MVC)
I think you mean to be using DropDownListFor - i.e. to display a list of items from which only one can be selected?
If you're actually looking for single-selection semantics in a listbox, that's trickier to do in MVC because it's Html helpers are geared entirely around listboxes being for multiple selection. Someone else on SO has asked a question about how to get a dropdown to look like a list box: How do I create a ListBox in ASP.NET MVC with single selection mode?.
Or you could generate the HTML for such a listbox yourself.
(Update) - Potentially needlessly detailed exposition(!)
The reason you don't get an exception first time round is probably because there was no value for id in ModelState when the HTML was generated. Here's the reflected MVC source (from SelectExtensions.SelectInternal) that's of interest (the GetSelectListWithDefaultValue call at the end is the source of your exception):
object obj =
allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string[])) :
htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string));
if (!flag && obj == null && !string.IsNullOrEmpty(name))
{
obj = htmlHelper.ViewData.Eval(name);
}
if (obj != null)
{
selectList =
SelectExtensions.GetSelectListWithDefaultValue(selectList, obj, allowMultiple);
}
Note first that the control variable allowMultiple is true in your case, because you've called ListBoxFor. selectList is the SelectList you create and pass as the second parameter. One of the things that MVC (unfortunately in some cases) does is to use ModelState to modify the select list you pass when re-displaying a view in order to ensure that values which were set in ModelState via a POST are re-selected when the view is reloaded (this is useful when page validation fails because you won't copy the values to your underlying model from ModelState, but the page should still show those values as being selected).
So as you can see on the first line, the model's current value for the expression/field you pass is fished out of model state; either as a string array or as a string. If that fails (returns null)then it makes another go to execute the expression (or similar) to grab the model value. If it gets a non-null value from there, it calls SelectExtensions.GetSelectListWithDefaultValue.
As I say - what you're trying to do will ultimately not work in either the case of Id or SupplierId (because they would need to be IEnumerable) but I believe this ModelState->Eval process is yielding a null value when you use Id, so the process of getting an 'adjusted' SelectList is skipped - so the exception doesn't get raised. The same is not true when you use SupplierId because I'll wager that there's either a value in ModelState at that point, or the ViewData.Eval successfully gets an integer value.
Not throwing an exception is not the same as working!.
End update
Try changing your property from int to int[]
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
Assuming above is the class used for binding the model , try changing the documentid property as below
public class SuppDocuments
{
public string Title { get; set; }
public int[] documentid { get; set; }
}
I have page with a simple table and advanced search form. I pass List<Customers> to the model:
View(List<Customers>);
So what is best way to pass and return data to the search form? I want to use validation or something but I think passing data through ViewData is not good idea. Any suggestions?
You should wrap all your data that is required by you view in a model specific to that view. The advantage to this is you could also include your search criteria in the model which would be empty at first but when your search posted, the model would automatically contain your search criteria so you could reload it when passing back the results. This will help maintain your state between post's as well.
This also allows all your view's data to be type safe where ViewData would not be.
Eg:
public class CustomerSearchViewModel
{
public List<Customer> Customers { get; set; }
// your search criteria if you want to include it
public string SearchFirstName { get; set; }
public string SearchLastName { get; set; }
public int SearchCustomerID { get; set; }
// etc...
}
When you return back the List<Customer> the search criteria would already be filled in your model from the post so your view can default the search criteria back to the corresponding controls (assuming your search results and search inputs controls are on the same view).
For example, in your post you would accept a CustomerSearchViewModel. Then all you need to do is get your list of customers and add it back to the model and return the same model.
// assuming you have accepted a CustomerSearchViewModel named model
model.Customers = GetCustomersForSearchCriteria(model.SearchFirstName,
model.SearchLastName, model.SearchCustomerID);
return View(model);
You could also add the validation attributes to your model properties to leverage the built in validation in MVC. This would not be possible if you were using ViewData to pass this data around.
You have to also consider the 'next guy'. It's cleaner when all the data that the view requires is located in a single class. This way they don't have to hunt through the code to discover if ViewData is being used and what data is actually being passed around in it.
ViewData is still an option for passing data but I try to minimize the use of it if at all possible.
Rather than passing just a list of items to your View, create a class which contains your list of items and any other data you might need, i.e. a ViewModel.
public class CustomerSearchViewModel {
public IEnumerable<Customer> Customers { get; set; }
public string SearchTerm { get; set; }
}
.....
var viewModel = new CustomerSearchViewModel {
Customers = customerList,
SearchTerm = searchTerm
};
return View(viewModel);