So I'm loosely following the Music Store tutorial. I created the StoreManagerController on pg. 54ish. And it created a view with the Create, Deleted, Edit, etc. It's saving some stuff to the database, namely my EditFor controls, but nothing else.
I have multiple DropDownListFor controls, populated by both tables in the database and also Active Directory user data. I'm not sure how to get these to save. Here is my abridged code. Thanks for the help.
View:
<div class="createTopInner">
<div class="editor-label">
#Html.LabelFor(model => model.test.Category)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.CategoryId, Model.CategoryItems, "")
#Html.ValidationMessageFor(model => model.test.Category)
</div>
</div>
Controller:
public ActionResult Create()
{
// These four lines get active directory users
ActiveDirectoryModel ads = new ActiveDirectoryModel();
ViewBag.assignedto = ads.FetchContacts();
ViewBag.coassignedto = ads.FetchContacts();
ViewBag.notifyto = ads.FetchContacts();
var model = Populate();
return View(model);
}
[HttpPost]
public ActionResult Create(CreateViewModel model)
{
if (ModelState.IsValid)
{
db.TestItems.AddObject(model.test);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(model);
}
public CreateViewModel Populate()
{
var model = new CreateViewModel
{
CategoryItems =
from c in new IntraEntities().CategoryItems.ToList()
select new SelectListItem
{
Text = c.Name,
Value = c.ID.ToString()
}
};
return model;
}
Model:
public class CreateViewModel
{
public Intra.Models.TestItem test{ get; set; }
public int CategoryId { get; set; }
public IEnumerable<SelectListItem> CategoryItems { get; set; }
}
The problem seems to be that, while most of your inputs map to properties on test, the CategoryId doesn't. Not knowing anything about your entity models, it's difficult to say, but I'd hazard a guess that you need to retrieve the corresponding Category from the database and add that to your TestItem instance before you persist it. If you do have a CategoryId property on your TestItem instance, you could just set it, but I'm guessing that you don't because otherwise you would have used it directly (as you do for the Category label) instead of adding a property to the view model.
If you have access to it / know much about stored procedures, it is much better to use Store procedures inside the database and then call them within Entity. It's much more loosely coupled and easier to make changes to without recompiling code.
Related
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>
I believe this will create a list in my HomeController. But not sure what calls it or where it goes in the Controller beside maybe the first Add ActionResult (GET method).
public static IEnumerable<SelectListItem> items()
{
using (oesacEntities_compact db = new oesacEntities_compact())
{
var query = from s in db.tblSponsors select new { s.SponsorID, s.BizName };
return query.AsEnumerable()
.Select(x => new SelectListItem
{
Value=x.SponsorID.ToString(),
Text = x.BizName
}).ToList();
}
}
I can't seem to send it to the Add view or to reference it from the Add view:
<div class="editor=field">
#Html.DropDownListFor(model => model.SponsorID,IEnumerable<SelectListItem> SelectList);
</div>
It seems so simple in other coding languages. I want to populate a pulldown with about 200 sponsor ID's for value, BizNames for text. For now at least. God help me after that when I want to show an Edit view with the value selected.
thankyou stackoverflow
You need to pass the SelectList to your view. Ideally your view model should include a property for the SelectList but you can (yuk) use ViewBag, for example
View Model
public class MyViewModel
{
public int SponsorID { get; set; }
// other properties
public SelectList SponsorList { get; set; }
}
Controller
public ActionResult SomeThing()
{
MyViewModel model = new MyViewModel();
// assign the select list
var sponsors = from s in db.tblSponsors;
model.SponsorList = new SelecList(sponsors, "SponsorID", "BizName");
return View(model);
}
View
#Html.DropDownListFor(model => model.SponsorID, Model.SponsorList);
or if you assigned the select list to ViewBag
#Html.DropDownListFor(model => model.SponsorID, (SelectList)ViewBag.SponsorList);
Using my model displaying a page works fine but the post does not return the bound model.
My classes:
public class ContactManager
{
public Contact Contact { get; set; }
public SelectList SalutationList { get; set; }
}
public class Contact
{
public int Id{get;set;}
public string FirstName{get; set;}
public SalutationType SalutationType{get; set;}
}
public class SalutationType
{
public int Id { get; set; }
public string Name { get; set; }
}
My View:
#model ViewModels.ContactManager
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Contact.Id)
#Html.DropDownListFor(model => model.Contact.SalutationType.Id, Model.SalutationList)
#Html.EditorFor(model => model.Contact.FirstName)
<input type="submit" value="Save" />
}
The issue seems to be in the DropDownListFor. The dropdown list displays correctly with the proper value but when I post this page the complete model is blank. If I simplify the DropDownListFor like this the values are posted as expected.
#html.DroDownListFor(model=>model.MyPlaceHolderProp, Model.SalutationList)
Is my model too complex? Am I not doing something correctly?
The models are based off of several tables using EF that I have created in a separate project. I am trying to avoid creating more classes/models then I have to.
You should post your controller action as well, as your model coming back as blank really has nothing to do with this. Changing the DropDownListFor definition one way or another should not effect the posting of any other values.
That said, you will run into another issue eventually here, so you need to regroup, anyways. You can't just post back the id value of a related item. Entity Framework will either complain that there's already an object with that id, or worse, if the object attaches, it will update the row with that id with the new posted value for Name, which in this case, is nothing, so it'll just clear it out.
When you create a relationship with a single item (a foreign key basically), if you don't specify a property to hold that foreign key value, Entity Framework creates one for you behind the scenes to track the relationship. In your case here, that means your Contacts table has a column named SalutationType_Id. However, there's no way from your class to directly access this value. This is why I recommend that you always provide an explicit property to handle the relationship:
[ForeignKey("SalutationType")]
public int SalutationTypeId { get; set; }
public SalutationType SalutationType { get; set; }
If you do that, then you can directly stuff the posted id there and Entity Framework will create the relationship.
#Html.DropDownListFor(m => m.Contact.SalutationTypeId, Model.SalutationList);
If you insist on keeping the key implicit, then you must create the relationship yourself, by creating a field on your view model to hold the posted value, then using that value to look up the SalutationType instance from the database, and then finally adding that to the Contact instance.
Add to your view model
public int SalutationTypeId { get; set; }
In your view
#Html.DropDownListFor(m => m.SalutationTypeId, Model.SalutationList)
In your POST action
var salutationType = db.SalutationTypes.Find(model.SalutationTypeId);
contact.SalutationType = salutationType;
You could do it this way. This may be the more "MVC best practice" way to handle it. Everything stays neatly in their models, and no manual IDs are required. The views are intended to be representations of the underlying models they are built on. If you are creating a view that has a form, then create a model that represents the form and use it in the view.
Revise your models like:
public class PostModel
{
public int ContactID { get; set; }
public int SalutationID { get; set; }
public string FirstName { get; set; }
}
public class PostView
{
public ContactManager contact { get; set; }
public PostModel post { get; set; }
}
Then create the PostView in the controller:
public ActionResult Index()
{
//create the PostView model
var pv = new PostView();
pv.ContactManager = contactManager;
pv.post = new PostView()
{
ContactID = contactManager.Contact.Id,
SalutationID = contactManager.SalutationType.Id,
FirstName = contactManager.Contact.FirstName
};
return View(pv);
}
Then the view could be like:
#model ViewModels.PostView
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.post.ContactID)
#Html.DropDownListFor(model => model.post.SalutationID, model.contact.SalutationList)
#Html.EditorFor(model => model.post.FirstName)
<input type="submit" value="Save" />
}
Then the post action in the controller:
[HttpPost]
public ActionResult Index(PostView pv)
{
//post code
//the posted data will be in pv.post
}
Have you considered using a custom model binder? Custom model binding isn't all that complicated for models that are still relatively simple, and you can handle the serialization/deserialization however you need to.
http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
http://ivonna.biz/blog/2012/2/2/custom-aspnet-model-binders-series,-part-3-subclassing-your-models.aspx
http://forums.asp.net/t/1944696.aspx?what+is+custom+model+binding+in+mvc
I am not sure this will help you... I wsa having a similar issue but I was using ajax to post back... anyway, I had forgotten to mark my binding class with the [Serializable] attribute.
so you might try
[Serializable]
public class Contract {
...
}
Again, I am using Json to post back to my controller so may not be related or help you. But, I guess could be worth a try.
I work with MVC and has one question, what is the best practice for building concatenated View Model properties? I can build concatenated field(FullName) in two places:
In Model View like this
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get {return FirstName+LastName;} }
}
public ActionResult Users()
{
var model = new User
{
FirstName = "Tomas",
LastName = "Saint",
});
return View(model);
}
In Controller
public class User
{
public string FullName { get; set; }
}
public ActionResult Users()
{
var model = new User
{
FullName = "Tomas" + "Saint";
});
return View(model);
}
If the calculations/functions are associated with formatting for the view then i would go ahead and put the functionality in the view model, like your full name property this is correct in the view model. However if you have any functions/calculations that require domain logic, then i dont think view models are the place for that and that can reside either in the domain model for reusability, or somewhere where else in your business logic layer.
So the short answer - formatting for the view in view models, any domain logic elsewhere.
Any methods or properties that calculate values for display purposes (like FullName in your example) belong in the ViewModel. Doing that gives you a clean, type-safe means of passing the data to the view and allows the view to access the property in a consistent way. This approach also has the advantage of making the property available wherever that particular ViewModel is used. That will not be the case if you add the property to your controller.
Example below using Razor syntax:
#model MvcApplication.Models.User
...
<div class="editor-label">
#Html.LabelFor(m => m.FullName)
</div>
<div class="editor-field">
#Html.DisplayFor(m => m.FullName)
</div>
I'm new to MVC and new to programming (in a general sense) as well.
I'm struggling with the concept of how to post multiple database entries into my database (via my controller) based on values I'm pulling from a MultiSelectList that I've populated.
Basically, I'm trying to create a simple model that contains a State and a City, and lets the user select and save multiple City values to their account based on State/City values pulled from a separate, static database.
Here's my Model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
namespace BidFetcher.Models
{
public class SPServiceLocation
{
public int SPServiceLocationID { get; set; }
public int SPCompanyAccountID { get; set; }
[Display(Name = "State")]
public string state { get; set; }
[Required]
[Display(Name = "Cities (select all that apply)")]
public string city { get; set; }
public virtual SPCompanyAccount SPCompanyAccount { get; set; }
}
}
Here is a snippet of my View data (including the multi-select list for city that I have no trouble populating):
<div class="editor-label">
#Html.LabelFor(model => model.state)
</div>
<div class="editor-field">
#Html.DropDownList("state", ViewBag.state as SelectList, "Select a state...", new { #class = "chzn-select", onchange = "CityChange()" })
#Html.ValidationMessageFor(model => model.state)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.city)
</div>
<div class="editor-field" id="SP_SelectCity">
#Html.ListBox("city", ViewBag.city as MultiSelectList, new { #class = "chzn-select", data_placeholder = "Select cities..." })
#Html.ValidationMessageFor(model => model.city)
</div>
<p>
<input type="submit" value="Submit" />
</p>
And here are my Create/Post Controllers:
public ActionResult Create()
{
ViewBag.sessionName = HttpContext.Session["SPCompanyName"].ToString();
var compID = HttpContext.Session["SPCompanyAccountID"].ToString();
ViewBag.companyID = compID;
ViewBag.state = new SelectList(simpleDB.simpleState, "simpleStateID", "simpleStateID");
ViewBag.city = new MultiSelectList(simpleDB.simpleCity, "cityFull", "cityFull");
return View();
}
[HttpPost]
public ActionResult Create(SPServiceLocation spservicelocation, FormCollection formValues)
{
if (ModelState.IsValid)
{
db.SPServiceLocation.Add(spservicelocation);
db.SaveChanges();
return RedirectToAction("Create", "SPServiceLocation");
}
return View(spservicelocation);
}
As you can see, I'm missing something in my [HttpPost] that'll allow me to save multiple values to the db database, but I'm not sure what exactly it is that's missing. I've seen some posts that explain this in terms of creating an IEnumerable list, but I guess I'm just not sure I need to do that since I'm already successfully populating my MultiSelectList via a database call.
Any help is greatly appreciated!
EDIT:
Based on the answers below, if I want to create multiple new database rows based on the results I collect from a MultiSelectList, I need to use Form collection to grab those results and parse them within my HttpPost:
And... I do not know how to do that. I assume something along these lines:
[HttpPost]
public ActionResult Create(SPServiceLocation spservicelocation, FormCollection formValues)
{
if (ModelState.IsValid)
{
foreach (var item in x)
{
db.SPServiceLocation.Add(spservicelocation);
db.SaveChanges();
}
return RedirectToAction("Create", "SPServiceLocation");
}
return View(spservicelocation);
}
Something along the lines of the above, where I create a collection based on my multiselectlist, break it into multiple variables, and step through my database.SaveChanges multiple times before I redirect?
Thanks!
The trick has nothing to do with your MVC code. You have to change either your Business Layer or Data Access Layer (DAL) code to read/write to 2 different databases.
I did the same project to answer one question sometime ago. Here it is: https://stackoverflow.com/a/7667172/538387
I checked the code, It's still working, you can download it and check it yourself.
When user submits the Create forms and application flows to [HttpPost] public ActionResult Create action, all of the form data live in formValues parameter.
You have to read those data from formValues (like: formValues["companyID"], build appropriate model object from them, and then update the db.