What Is The Best Practice To Deal With MVC Post Models? - c#

I'm pretty new to MVC and still confused what the best and proper way for 2 cases for with same result.
Let's say some user should add new sub category for specific root category.
Case 1:
SubCategory is a mapped class by EF where all properties not nullable.
Controler:
[Authorize]
public ActionResult Create()
{
SubCategory subCategory = new SubCategory();
subCategory.RootCategoryID = 1;
return View(subCategory);
}
[Authorize]
[HttpPost]
public ActionResult Create(SubCategory thisSubCategory)
{
if (ModelState.IsValid)
{
//And some BL logic called here to handle new object...
}
}
View:
#Html.HiddenFor(model => model.ID)
#Html.HiddenFor(model => model.RootCategoryID)
<h3>Sub Category Name: </h3>
#Html.EditorFor(model => model.CategoryName)
#Html.ValidationMessageFor(model => model.CtaegoryName)
<input id="btnAdd" type="submit" value="Add" />
Case 2:
Add helper class as controller's model and populate EF object after post
Controler:
class SubCategoryHelper
{
public string Name { get; set; }
}
[Authorize]
public ActionResult Create()
{
SubCategoryHelper subCategory = new SubCategoryHelper();
return View(subCategory);
}
[Authorize]
[HttpPost]
public ActionResult Create(SubCategoryHelper thisSubCategory)
{
if (ModelState.IsValid)
{
SubCategory newSubCategory = new SubCategory();
newSubCategory.RootCategoryID = 1;
newSubCategory.CtaegoryName = thisSubCategory.Name;
//And some BL logic called here to handle new object...
}
}
View:
Sub Category Name:
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
<input id="btnAdd" type="submit" value="Add" />
Both ways makes the same, but the first way looks less secure because of the hiddens that could be changed on the client side.
The second way much longer, imagine the same way for rich objects such as Client or Product...
What should I choose? Or there is some other way?

The first case is good for simplicity. If you extend your model you will have to do changes on less places. It is not less secure. You can bypass creating or binding the hidden input fields by several ways.
Use BindAttribute to bypass binding of property:
ActionResult Create([Bind(Exclude = "RootCategoryId")]
SubCategoryHelper thisSubCategory) {//....}
Or ScaffoldColumnAttribute on the model class property (e.g. when you use edit templates):
[ScaffoldColumn(false)]
public int RootCategoryId {get; set;}
Or just simple do not expose it (as you did in your example using the Html.HiddenInput helper).
The second approach, you have described, is often called ViewModel pattern. It encourages seperation of your Presentation and Domain layer. The advantage is, that your domain model will not be polluted with presentation layer specific code (like various display attributes etc.). Hovewer it brings another overhead of mapping between domain models and view models.
There is probably no genarel rule of thumb. It depends on type of your appliction.
If it is basically some simple data driven CRUD application, you can easily stay with the first one. Nevertheless, when your application becomes larger, you will definetly appreciate the freedom of your hands on separate layers. And if your BLL code is used with some other kind of "client" (web service, desktop etc.) except the ASP MVC I would defintely go with the second option.
I also suggest to read this great article: Is layering worth mapping

I always go with Case 2, whether it's a small project I work on or a big project, I always separate my data layer (entity framework) and my UI layer. Especially if you are using Entity Framework, because those objects can get huge, and it's a lot of crap that you are passing around that you more often don't need.
Instead of calling it a Helper class, call them ViewModel or Model. In your case, SubCategoryViewModel.
public class SubCategoryViewModel
{
public int Id {get;set;}
public int RootCategoryId {get;set;}
[Required]
public string Name { get; set; }
}
[Authorize]
public ActionResult Create()
{
var subCategoryViewModel = new SubCategoryViewModel();
return View(subCategoryViewModel);
}
[Authorize]
[HttpPost]
public ActionResult Create(SubCategoryViewModel viewModel)
{
if (ModelState.IsValid)
{
var subCategory = new SubCategory();
subCategory.RootCategoryID = 1;
subCategory.CategoryName = viewModel.Name;
//And some BL logic called here to handle new object...
}
}

Related

How to pass data from viewmodel to?

I am new to MVC and trying to understand viewmodels.
I have Staff, Service, BookingSlot, Appointments and the ApplicationUser entities. I have the following viewmodel:
public class AppointmentBookingViewModel
{
[Display (Name ="Select Staff")]
public int StaffId { get; set; }
public IEnumerable<Staff> Staffs { get; set; }
[Display(Name = "Select Service")]
public int ServiceId { get; set; }
public IEnumerable<Service> Services { get; set; }
[Display(Name = "Select Slot")]
public int BookingSlotId { get; set; }
public IEnumerable<BookingSlot> BookingSlots { get; set; }
}
This is the controller:
public class AppointmentBookingController : Controller
{
private readonly SalonContext _context;
private AppointmentBookingViewModel _appointmentBookingViewModel = new AppointmentBookingViewModel();
public AppointmentBookingController(SalonContext context)
{
_context = context;
ConfigureViewModel(_appointmentBookingViewModel);
}
public void ConfigureViewModel(AppointmentBookingViewModel appointmentBookingViewModel)
{
appointmentBookingViewModel.Staffs = _context.Staffs;
appointmentBookingViewModel.Services = _context.Services;
appointmentBookingViewModel.BookingSlots = _context.BookingSlots;
}
// GET: AppointmentBooking
public ActionResult Index()
{
return View(_appointmentBookingViewModel);
}
}
My question is, how can I create a form in the view and post the data to the Appointments table, the following doesn't work.
#model HairStudio.Services.ViewModels.AppointmentBooking.AppointmentBookingViewModel
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col-12">
<form asp-action="Create">
<div class="form-group">
<label asp-for="ServiceId" class="control-label"></label>
<select asp-for="ServiceId" class="form-control"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
You already directed your form to action called "Create" with asp-action attribute, but there is no such action in your controller. Submitting a form sends a HTTP POST request, which needs to be handled by your controller. Therefore, add a Create() method in your AppointmentBookingController:
// POST: Create
public IActionResult Create(AppointmentBookingViewModel appointmentViewModel)
{
if (!ModelState.IsValid)
{
// Server side validation of form has failed.
// Return to the calling view and inform the user about the errors.
return View(appointmentViewModel, "Index");
}
return View(appointmentViewModel, "<NAME_OF_YOUR_CREATED_APPOINTMENT_VIEW>");
}
Consider redirecting after successfully accepting a HTTP POST request according to a design pattern Post/Redirect/Get.
Also, take a look at this part of ASP.NET Core documentation about working with forms. I'm sure you'll find there something of value.
There's nothing magical about a view model. It's just a class. The idea is that the entity class (i.e. the thing you're persisting to the database via Entity Framework) should be concerned only with the needs of the database. A view can and often does have an entirely different set of needs, so you create a class specifically for that: the view model. This is just basic SRP (single-responsibility principle): a single class shouldn't try to do too much.
Then, you simply need a way to bridge the two. In other words, you need to copy values from the entity to the view model and vice versa. That process is called mapping, and can be achieved in a number of different ways. The most common approach is to use a third-party library like AutoMapper. However, you can also just manually map over each value or even use something akin to the factory pattern, where you have another class that holds the knowledge for how to do the mapping and can spit out an entity from a view model and vice versa.
Now, it's not really possible to give you exact guidance because we don't have your entity(ies), but you seem to be wanting to pick a particular Staff, Service and BookingSlot and associate that with the Appointment you're creating. It's not critical, but for efficiency, you should not be carrying around the full set of all these entities on your view model. All you need is an IEnumerable<SelectListItem>, which allows you to use much more efficient queries:
Instead of the Staffs property, for example:
public IEnumerable<SelectListItem> StaffOptions { get; set; }
Then:
model.StaffOptions = await _context.Staffs.AsNoTracking()
.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() })
.ToListAsync();
In your view:
<select asp-for="StaffId" asp-items="#Model.StaffOptions" class="form-control"></select>

How can I post two or more models in one form?

I'm working on an internet lesson plans application for a project. The lesson plan is built from following models (generated with Entity Framework in Database First approach):
public partial class Subject
{
public int Id { get; set; }
public string Hour { get; set; }
public string Name { get; set; }
public int DayId { get; set; }
[Required]
public int PlanId { get; set; }
public virtual Day Day { get; set; }
public virtual Plan Plan { get; set; }
}
public partial class Plan
{
public Plan()
{
this.Przedmiot = new HashSet<Subjects>();
}
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Subject> Subject { get; set; }
}
I have no problems with displaying both models in one View, but I can't seem to figure out how to post both models to database when creating new plan. I want my View to look something like this:
So my question is what's the best approach here, and how can I create one record in Plan table in database, and many Subject records linked to it in this one view.
Edit:
Code with my display View as requested (omitted unnecessary parts because it's rather long):
#model IEnumerable<Lesson_plan.DAL.Subject>
<table style="border: 1px solid black; margin: 40px; width: 100%;">
<tr>
<th>Hours</th>
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th>Saturday</th>
<th>Sunday</th>
</tr>
#{
if (Model != null)
{
var r = 1;
var t = 1;
List<string> hours = new List<string>();
foreach (var subject in Model)
{
if (!hours.Contains(subject.Hour))
{
<tr>
<td>
<textarea>
#Html.DisplayFor(modelItem => subjest.Hour)
#{ hours.Add(subject.Hour); }
</textarea>
</td>
<td>
<textarea>
#foreach (var subjectName in Model)
{
if (subjectName.Day.DayOfTheWeek.Equals("Monday") &&
subject.Hour.Equals(subjetName.Hour))
{
#Html.DisplayFor(modelItem => subject.Name)
}
}
</textarea>
</td>
//and so on for every day
}
</tr>
r++;
}
}
}
}
</table>
Code of my Controller class (I did some experiments with Create method, but I'm posting original method here):
namespace Lesson_plan.Controllers
{
public class PlansController : Controller
{
private readonly LessonPlanEntities db = new LessonPlanEntities();
// GET: Plans
public ActionResult Index()
{
var plans = db.Plan.ToList();
return View(plans);
}
// GET: Plans/Details/5
public ActionResult Details(int? id)
{
if (id == null)
return Create();
var subjects = db.Subject.
Where(x => x.PlanId == id).
OrderByDescending(x => x.Hour).ToList();
if (subjects.Count > 0)
ViewBag.Title = subjects[0].Plan.Name;
return View(subjects);
}
// GET: Plans/Create
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Plan plan)
{
if (ModelState.IsValid)
{
db.Plan.Add(plan);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(plan);
}
// GET: Plans/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
var plan = db.Plan.Find(id);
if (plan == null)
return HttpNotFound();
return View(plan);
}
// POST: Plans/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,Nazwa")] Plan plan)
{
if (ModelState.IsValid)
{
db.Entry(plan).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(plan);
}
// GET: Plans/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
var plan = db.Plan.Find(id);
if (plan == null)
return HttpNotFound();
return View(plan);
}
// POST: Plans/Delete/5
[HttpPost]
[ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
var plan = db.Plan.Find(id);
db.Plan.Remove(plan);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
if (disposing)
db.Dispose();
base.Dispose(disposing);
}
}
}
Edit2
Code for the Create View with form:
#model Lesson_plan.DAL.Plan
#using (Html.BeginForm("Create", "Plans"))
{
<div>
#Html.LabelFor(plan => plan.Name)<br/>
#Html.TextAreaFor(plan => plan.Name)
</div>
<table style="border: 1px solid black; margin: 40px; width: 100%;">
<tr>
<th>Hours</th>
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th>Saturday</th>
<th>Sunday</th>
</tr>
#{
for (int i = 0; i < 7; i++)
{
<tr>
//what sould i put here ?
<td><textarea></textarea></td>
<th><textarea></textarea></th>
<th><textarea></textarea></th>
<th><textarea></textarea></th>
<th><textarea></textarea></th>
<th><textarea></textarea></th>
<th><textarea></textarea></th>
<th><textarea></textarea></th>
</tr>
}
}
</table>
<p>
<a type="button" class="btn btn-info" href=#Url.Action("Index")>Back</a>
<input type="submit" value="Create"/>
</p>
}
The best approach is to design a ViewModel class that can hold all the data on your Form that needs to be submitted, for both Plan(s) and/or Subject(s).
In the Action Method that receives this ViewModel you can pick apart the data, and based on that do whatever inserts and/or updates are needed to store it in the database.
Only the Controller should have knowledge of your database classes. All mapping between ViewModel and Database objects should be done by the controller, and the View should not be bothered or hampered by this.
Multiple models can be sent to the view by combining them into a viewmodel. However, your issues seem to be different, so here goes the long answer:
As for the DB design question, I'm assuming that you're using a relational storage and one subject can be present in multiple plans so a many-to-many should be in place. You can read more on the subject here.
As for your web app, you can usually simply create a ViewModel, that contains both your desired models. If you hadn't elaborated on your question, that would be the correct answer. However, in your case, your two "model" classes are already linked, so your model can simply be a Plan.
Since I'm assuming you're using EF6 (based on your generated classes) your subjects for the plan will be posted back alongside the plan.
What I'd do is cache the currently existing subjects somewhere in your app using a singleton, and then on postback you should iterate through the cached subjects for your plan, checking if they already exist in your DB. If they don't - insert them first, if they do - simply add them to your ICollection in the plan. Then you should probably know how to insert the Plan using EF.
There are probably going to be much more bumps along the way, but you can raise new questions about those - your question as it is now is way too broad.
I'm trying to help you get something working, although it's probably not going to be ideal. Once you get it to work, I'd suggest reading on where to put business logic (and how to keep your controller small). This article could be a good start.
Another thing I'd suggest is to keep your data access in a separate layer. Goodle "3-layer architecture" and also check this video on the repository pattern.
Good luck!
** Post OP-edit **
Ok, looking at your code there seem to be a lot of issues, that you'll probably want to separate into multiple questions. Here's a start:
There is no form on your View and you need a form and a post button on your View, and a version of your Action method on the Controller marked with [Post]. I recommend this tutorial for asp.mvc core and this for older versions. If you want different pages for your "edit" and "display" views, simply create separate ones, but when you're creating, you'll want to have a form.
For the time being, you should use a Plan as a viewModel. Typically this is not ideal, but to explain why I'd need to write a book as an answer. The resources linked should help you understand that part.
You seem to be using an older version of asp.net mvc (maybe I'm wrong? please tell us which one). If you want to ADD new Subjects on specific dates on the client, you'll also have to use javascript (jQuery) - or a full client-side library such as React or Angular. If you're not familiar with full frameworks stick to JQuery for now. Examine this question and answer to get an idea of how to achieve that.
You might want to separate your API methods and your plain old MVC methods into different projects.
** Edit 2 **
Seeing your Plan creation code now, you seem to have correctly read that #Html.TextAreaFor(plan => plan.Name) is the way to create an input that can be posted to the server.
However you're only using it for your plan name.
Instead of using textarea below (I'm assuming these are for the Subject names) use something like
#for(int i = 0; i < plan.Subject.Count(); i++)
{
#Html.TextAreaFor(plan => plan.Subject[i].Name)
}
Assuming the Plan you passed as a model to the view has any subjects, this will be the way to list them and make them editable.
Now, it seems that you want to separate them based on the day of the week, so you might want to do some filtering, for example:
var monday = 1;
#for(int i = 0; i < plan.Subject.Where(s => s.DayId == monday).OrderBy(s => s.Hour).Count(); i++)
And paste this into each of the seven columns. This will now create textareas for every subject that already exists in there.
As for dynamically creating new ones on the view, refer to point #3 above. If you don't have any subjects yet, create some fake ones in your db, just so you can render some on the client and check their source code, then follow the article I linked in #3.

How to retrieve multiple Models From Single view

Is there a way to obtain a model object from the view into the controller without using strongly typed HTML helpers. The View uses the Account entity But need to post the form and retrieve the account entity of the variable running in a foreach loop used to generate the table/grid of values. The information from the dropdown is used for a different entity hence why its not strongly typed.
public class HomeController : Controller
{
// GET: Home
ModelContext model = new ModelContext();
[HttpGet]
public ActionResult Index()
{
return View(model.accounts.ToList());
}
[HttpPost]
public ActionResult Index(Account account,List<string> status,string name)
{
// return student object
return View();
}
}
The rest of the code in the View runs on a foreach loop to generate an html table enclosed in a #HTML.BeginForm
<td>
#Html.DropDownList("status",
new SelectList(Enum.GetValues(typeof(ProjectName.Models.Code))),
"...",
new { #class = "form-control" })
</td>
<td>
#Html.TextArea("comment");
</td>
You should be created a parent view model that includes both of these view models that you'll be needed in your view. For example:
public class ViewModel
{
public ProjectName.Models.Code Code {get; set;}
public ProjectName.Models.Account Account {get; set;}
}
Also, please have a look MVC - Multiple models in a view. I think it's good for you.
Hope this help!

Using ViewModel to setup MVC Form but want to bind to object of the viewmodel in the postback. Is this possible?

Just need to know if this is possible to do or what exactly is standard practice in MVC as this is my first large scale MVC application.
So I've got a form I want the user to be able to edit and on that form page to pull up all the necessary data I need I'm bringing in a viewmodel,
public class Tier2IssueFormViewModel
{
public Tier2IssueDTO Tier2Issue { get; set; }
public IEnumerable<SelectListItem> VersionList { get; set; }
public IEnumerable<SelectListItem> BugList { get; set; }
public IEnumerable<SelectListItem> IssueStatusList { get; set; }
}
Then once I've finished collecting the form data from the user using things like,
#Html.TextBoxFor(m => m.Tier2Issue.Tier2Notes, new { #class = "form-control"})
#Html.DropDownListFor(m => m.Tier2Issue.FishbowlVersion, Model.VersionList, "Select Application Version")
#Html.HiddenFor(m => m.Tier2Issue.ID)
I want to post back to this action with the following signature for my model to bind to,
[HttpPost]
[Route("Issues/{id}/Edit")]
public ActionResult EditIssue(Tier2IssueDTO model)
{
...
// Update DB with the DTO model
...
}
But so far nothing really gets bound to this object. I thought the model binder might be smart enough to pair the two but I'm guessing this logic is incorrect. So I'm currently doing a workaround by using this,
[HttpPost]
[Route("Issues/{id}/Edit")]
public ActionResult EditIssue(Tier2IssueFormViewModel model)
{
...
// Get Tier2IssueDTO values from viewmodel
// Update DB with the DTO model
...
}
I mean it works, but it seems odd to me that you would model bind to a view model. Is this standard practice or is there a way to bind to an object contained within the viewmodel directly?
This will not work because the input text box names are differnt from the model inside your action, ex: the text box will have a name Tier2Issue.Tier2Notes while the model parameter in your action is expecting a property name Tier2Notes only without the Tier2Issue prefix.
You can overcome this issue by either making the model the same as the action parameter or give an explicit name and value to the text box, ex:
#Html.TextBox("Tier2Notes",Model.Tier2Issue.Tier2Notes, new { #class = "form-control"})
This should make it work
You have the right of it. It often seems pretty repetitive to have a viewmodel, dto and entity that all seem to have the same properties, but they all do different jobs an usually end up diverging a bit. A dto could act as a viewmodel, but it's a square peg in a round hole. If you're not using automapper to map these objects to one an other (this may be opinion baesed but it's broadly shared) - then use automapper to save you mindless keystrokes.

How do I grab data from a many-to-many relationship? (razor)

I just successfully set up a many-to-many relationship between BlogPosts and Topics in Entity Framework code first approach. So there are a list of topics ("CSS", "HTML", "ASP.NET") that a BlogPost can have many of and vice versa. So currently I had EF create 3 tables, the middle table being the id of both the BlogPost and the Topic itself.
Now I am in the Razor view of my homepage.
#model MvcBlog.Models.MyModel
#foreach (var post in Model.Posts)
{
<div class="blogpost">
<h2>#Html.DisplayFor(modelItem => post.Title)</h2>
<div class="post_info">#Html.DisplayFor(modelItem => post.DateCreated)<span class="right">Blog</span></div>
<p>#Html.Raw(post.Content)</p>
<div class="post_close">
<span class="left">
***********************
</span>
<span class="right"><img src="Content/images/comment.jpg" alt="" /> 0 comments</span>
</div>
</div>
}
All of the above works just fine, but I want to replace the * with the topics associated with this particular post. I can't seem to figure this out. Do I have to pass the model differently from the controller? Currently I am passing the entire DB to this page as it will be using various info from different tables. I am just lost on this one. Any help would be really appreciated! (Obviously I want to do something similar with comments)
Thanks!
No, no, no, do NOT pass the entire database to the view. You need to be abstracting your view data from your database. Create a view model containing just the data you need for this view in the format best suited for the view to consume it. Use your controller or model code, depending on whether you believe in fat or thin controllers, to transform the data from the database into the view model. The whole point of MVC is separation of concerns and if you let your data model leak into the view code, you will be losing this basic idea and lose the benefits of loose coupling between the various layers.
To get you started on the recommended course of action. Your view model will be a normal class:
public class PostViewModel
{
public string Title { get; set; }
public DateTime DateCreated { get; set; }
public List<Topic> Topics { get; set; }
public List<Comment> Comments { get; set; }
}
In your controller, you populate what you need for the view
public ActionResult Index()
{
// assuming entity framework
List<PostViewModel> posts = (from p in context.Set<Post>()
select new PostViewModel {
Title = p.Title,
DateCreated = p.DateCreated,
Topics = p.Topics
}).ToList();
return View(posts);
}
And in your view
#model List<PostViewModel>
#foreach(Post post in Model)
{
#Html.DisplayFor(m=>m.Title)
#foreach(Topic topic in post.Topics)
{
}
}
Do I have to pass the model differently from the controller?
Yes. Make a model specifically for the needs of your view (a view model). Fill in the correct object graphs there (which blog goes to which topic). Then pass that instantiated object into the view and reference those objects in your view.

Categories