How can one efficiently modify a collection within a ViewModel? - c#

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.

Related

Asp.Net Core MVC 7 C# - Recommended approach of passing data to and from controller from a page that has multiple forms and states

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)

How to structure creation views for complicated objects in asp.net mvc?

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.

How to get dynamic form submission record count in Orchard CMS

I made a register page with dynamic form in Orchard CMS, and received new requirements of checking record count.
I have no idea about how to do this, I looked into the SubmissionAdminController.cs in Orchard.DynamicForms.Controllers folder, but still could not find a way.
I'm thinking to get the record count from my cshtml view page and check it in different parts, is it possible?
To get the record count of the stored submissions, inject or resolve an IRepository<Submission>, and use the Count() method to count all items. Note that the Count() method accepts an expression, which allows you to filter by form name for example. For reference, this is what the Submission class looks like:
namespace Orchard.DynamicForms.Models {
public class Submission {
public virtual int Id { get; set; }
public virtual string FormName { get; set; }
[StringLengthMax]
public virtual string FormData { get; set; }
public virtual DateTime CreatedUtc { get; set; }
}
}
When you have an IRepository<Submission>, this is how you would count all submissions in a form called "MyForm":
var count = submissionRepository.Count(x => x.FormName == "MyForm");
If you don't have a controller or custom part or anything to inject this IRepository into, then you could resolve the repository directly form your view like this:
#{
var submissionRepository = WorkContext.Resolve<IRepository<Submission>>();
var submissionCount = submissionRepository.Count(x => x.FormName == "MyForm");
}
Make sure to import the proper namespaces:
Orchard.DynamicForms.Models for Submission
Orchard.Data for IRepository<T>
However, if you need to display this number in multiple places, it's best to create a shape so that you can reuse it. Even better would be to not resolve the repository from the shape template directly, but via an IShapeTableProvider. The primary reason for that becomes clear when you start overriding your shape template or start providing shape alternates, both in which cases you'll have duplicate logic in all of your shape templates, which isn't very DRY of course. And there's the more philosophical issue of separation of concerns: you don't want data access code in your views. Rather, use that code from a controller, driver or shape table provider.

Most efficient way to convert a object to another (Model to ViewModel)

Suppose I have a model with 20 fields, and in my index page, I want to list all models that are stored in my database.
In index page, instead of listing all fields of the model, I only to list 3 fields.
So, I make two class:
class CompleteModel {
public int Id { get; set; }
public string Field01 { get; set; }
public string Field02 { get; set; }
public string Field03 { get; set; }
public string Field04 { get; set; }
public string Field05 { get; set; }
...
public string Field20 { get; set; }
}
now, in my Controller, I can use:
await _context.CompleteModel.ToListAsync();
but I feel that it does not seem to be the right way to do it, because I'm getting all fields and using only 3 fields.
So, I made this code:
class ViewModel {
public string Field02 { get; set; }
public string Field04 { get; set; }
public string Field08 { get; set; }
}
var result = _context.CompleteModel.Select(
x => new {
x.Field02,
x.Field04,
x.Field08
}).ToListAsync();
var listResults = new List<IndexViewModel>();
if (result != null)
{
listResults.AddRange(results.Select(x => new IndexViewModel
{
Field02 = x.Field02,
Field04 = x.Field04,
Field08 = x.Field08
}));
}
I think this is a lot of code to do this.
First, I selected all the fields that I want, then, copied everything to another object.
There's a "more directly" way to do the same thing?
Like:
_context.CompleteModel.Select(x => new IndexViewModel { Field02, Field04, Field08 });
You could use AutoMapper to reduce the boiler plate so you're not manually copying field values over.
If you include the AutoMapper NuGet package then you'd need to have the following in your startup somewhere to configure it for your classes:
Mapper.Initialize(cfg => cfg.CreateMap<CompleteModel, ViewModel>());
You could then do something like the following:
var results = await _context.CompleteModel.ToListAsync();
var viewModelResults = results.Select(Mapper.Map<ViewModel>).ToList();
There are a lot of configuration options for the package so do take a look at the documentation to see if it suits your needs and determine the best way to use it if it does.
In my view this is one of the weaknesses of over abstraction and layering. The VM contains the data that is valuable to your application within the context of use (screen, process etc). The data model contains all the data that could be stored that might be relevant. At some point you need to match the two.
Use EF Projection to fetch only the data you need from the database into projected data model classes (using the EF POCO layer to define the query, but not to store the resultant data).
Map the projected classes onto your VM, if there is a naieve mapping, using Automapper or similar. However unless you are just writing CRUD screens a simple field by field mapping is of little value; the data you fetch from your data store via EF is in its raw, probably relational form. The data required by your VM is probably not going to fit that form very neatly (again, unless you are doing a simple CRUD form), so you are going to need to add some value by coding the relationship between the data store and the View Model.
I think concentrating on the count of lines of code would lead to the wrong approach. I think you can look at that code and ask "is it adding any value". If you can delegate the task to Automapper, then great; but your VM isn't really pulling its weight other than adding some validation annotation if you can consistently delegate the task of data model to VM data copying.

Pre-processing Form data before Model validation in MVC

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

Categories