I am having difficulty with my understanding of MVC coming from an aspx world.
I have a Model called CustomerGarment. This has a Order and a Customer along with a few garments.
public class CustomerGarment
{
public int CustomerGarmentId { get; set; }
public virtual Customer Customer { get; set; }
public virtual Order Order { get; set; }
public virtual GarmentJacket GarmentJacket { get; set; }
public virtual GarmentShirt GarmentShirt { get; set; }
}
I have a method for get and post. When the page loads, it creates a new CustomerGarment instance and querys the database to fill the Customer and Order variables. I then use the viewbag to show on the screen a list of GarmentJackets and GarmentShirts
The page then views and using the view I can access the model perfectly. Drop downs load with the viewbag contents and I can access all Customer and Order variables using the model I have passed.
The problem I then face is when I use the HttpPost. The model is not passed back with the information I passed to it.
public ActionResult AddGarments(int orderId, int customerId)
{
CustomerGarment cg = new CustomerGarment();
cg.Order = (from a in db.Orders where a.OrderId == orderId select a).FirstOrDefault();
cg.Customer = (from a in db.Customers where a.CustomerId == customerId select a).FirstOrDefault();
var jackets = from a in db.GarmentJackets orderby a.Type, a.SleeveLengthInches, a.ChestSizeInches select a;
var shirts= from a in db.GarmentKilts orderby a.PrimarySize, a.DropLength select a;
ViewBag.GarmentJacket = new SelectList(jackets, "GarmentJacketId", "GarmentJacketId");
ViewBag.GarmentShirt = new SelectList(shirts, "GarmentShirtId", "GarmentShirtId");
return View(cg);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddGarments(CustomerGarment cg)
{
// Here, I do not have the customer info for example
db.CustomerGarments.Add(cg);
db.SaveChanges();
return RedirectToAction("Index");
return View(cg);
}
This is a bit of my view
#Html.HiddenFor(model => model.Order.OrderId)
#Html.HiddenFor(model => model.Order.CustomerId)
<div class="display-field">
#Html.DisplayFor(model => model.Customer.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.GarmentJacket, "Jacket")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.GarmentJacket, (SelectList)ViewBag.GarmentJacket, new {style="width:312px;height:30px;margin-top:2px;margin-bottom:5px"})
</div>
EDIT
My Garment Jacket Model
public class GarmentJacket : Garment
{
public int GarmentJacketId { get; set; }
[Required]
public string Type { get; set; }
[Required]
[Display(Name = "Chest Size")]
public int ChestSizeInches { get; set; }
[Required]
[Display(Name = "Sleeve Length")]
public int SleeveLengthInches { get; set; }
}
public class Garment
{
[DataType(DataType.Date)]
public DateTime? DateRetired { get; set; }
[Required]
public string Barcode { get; set; }
[Required]
public bool Adults { get; set; }
}
In your CustomerGarment class, you should have:
public class CustomerGarment
{
public int CustomerGarmentId { get; set; }
public int CustomerId { get; set; }
public int OrderId { get; set; }
public int GarmentJacketId { get; set; }
public int GarmentShirtId { get; set; }
public virtual Customer Customer { get; set; }
public virtual Order Order { get; set; }
public virtual GarmentJacket GarmentJacket { get; set; }
public virtual GarmentShirt GarmentShirt { get; set; }
}
And, then, in your View, your DropDownList will look like:
#Html.DropDownListFor(m => m.GarmentJacketId, (SelectList)ViewBag.GarmentJacket, new {style="width:312px;height:30px;margin-top:2px;margin-bottom:5px"})
Your DropDownList only posts one value, which is the GarmentJacketId. You can't bind that Id to the whole GarmentJacket class.
By the way, you also need to replace your hidden inputs with these:
#Html.HiddenFor(model => model.OrderId)
#Html.HiddenFor(model => model.CustomerId)
I think I know your problem. As you suggested in you comment above you need to post everything you want retained in the view. This is one of the differences beteween webforms and MVC, webforms has viewstate that could contain information that you don't explicitly add to the view and post back, giving the impression of state. In MVC you have to add it to the view.
On the other hand you don't need to pass in more information than you need either. You pass inn the customerId as a hidden field. On post method you get the customer from the db using the Id, then you add the order to the customer.
I have some questions about your design, but given that a customer holds a collection of Orders, you could do something like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddGarments(CustomerGarment cg)
{
// Get the customer from the database
var customer = db.Customers.Find(c=>c.id==cb.Customer.Id)
var order = new Order();
//Create your order here using information from CustomerGarment model
//If the model already holds a valid Order object then just add it.
//i.e. you could get a Garment object from the DB using the GarmentId from
//the ViewModel if you really need more than just the Id to create the order
customer.Orders.Add(order);
db.SaveChanges();
return RedirectToAction("Index");
}
Related
I am working on a CRUD ASP.NET Core MVC application. I have two entities Product and Categrory, i want to populate a DropDownlist from model "Category" in the "Product" View. Here is my code:
public class Product
{
[Key]
public int ProductId { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
[ForeignKey("CategoryId")]
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
[Key]
public int CategoryId { get; set; }
[Required]
public string CategoryName { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
ProductController.cs:
[HttpGet]
public IActionResult Create()
{
List<Category> categories = _dbcontext.Category.ToList();
ViewBag.bpCategories = new SelectList(categories, "CategoryId", "Category");
Product product = new Product();
return View(product);
}
and in Create.cshtml i used this code to display the Dropdownlist:
<div class="form-group">
<label asp-for="CategoryId" class="control-label"></label>
<select asp-for="CategoryId" class="form-control" asp-items="ViewBag.bpCategories"></select>
</div>
But this code throws Nullreference exception. Any suggestions??
Maybe an error comes from SelectList constructor. Try this:
ViewBag.bpCategories = new SelectList(categories, "CategoryId", "CategoryName");
Use "CategoryName" as text value instead of "Category".There is no Category property in your Category class.
The third parameter is the data text field. Check here:
https://learn.microsoft.com/en-us/dotnet/api/system.web.mvc.selectlist.-ctor?view=aspnet-mvc-5.2#system-web-mvc-selectlist-ctor(system-collections-ienumerable-system-string-system-string)
I have a class with with one-to-many relationship and trying to display the virtual object in the view. I am fairly new to MVC and EF so this maybe something simple.
I am trying to display a drop down list for PODObject on "Request/Create" action view.
Here are the models
public class PODObject
{
public int PODObjectID { set; get; }
public string name { set; get; }
public string CustomAttribute1 { set; get; }
public string CustomAttribute2 { set; get; }
public string CustomAttribute3 { set; get; }
public string CustomAttribute4 { set; get; }
public string CustomAttribute5 { set; get; }
public virtual ICollection<BookingObject> BookingObjectspublic { set; get; }
}
public class RequestsQueueObject
{
// Request Attributes
public int RequestsQueueObjectID { set; get; }
public string CustomAttribute1 { set; get; }
public string CustomAttribute2 { set; get; }
public string CustomAttribute3 { set; get; }
public string CustomAttribute4 { set; get; }
public string CustomAttribute5 { set; get; }
public int PODObjectID { set; get; }
public virtual PODObject PODObject { set; get; }
}
Here is the controller Create action
public ActionResult Create([Bind(Include="RequestsQueueObjectID,POCPODAccess,CustomAttribute1,CustomAttribute2,CustomAttribute3,CustomAttribute4,CustomAttribute5")] RequestsQueueObject requestsqueueobject)
{
if (ModelState.IsValid)
{
db.RequestsQueueObjects.Add(requestsqueueobject);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(requestsqueueobject);
}
Here is the cshtml
<div class="form-group">
#Html.DropDownListFor(model => model.PODObject, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PODObject)
#Html.ValidationMessageFor(model => model.PODObject)
</div>
</div>
Maybe I am wrong but your PODOObject doesn't look like a List, an Array or whatever, so you can't really use it as a source for your dropdown...
I think what you're looking for is being able to pick from a list of existing PODObject instances to set as the related item for RequestQueueObject. You've got a few issues you need to clear up for that.
You have a property on RequestQueueObject to hold the id for the relationship with PODObject, but this currently isn't being used. Entity Framework's convention is to look for an property with the naming pattern [RelatedProperty][RelatedClassIdProperty]. In your scenario, that would be PODObjectPODObjectID or PODObject_PODObjectID. You can clear that up by adding the ForeignKey attribute to your PODObjectID property:
[ForeignKey("PODObject")]
public int PODObjectID { get; set; }
Html.DropDownListFor requires an IEnumerable<SelectListItem> for its second parameter, which you don't currently have. MVC won't do this for you; you need to create the list of choices yourself. Ideally, you would use a view model with this as a property, but for the time being, you can use ViewBag:
In your action
var podObjectChoices = db.PODObjects.Select(m => new SelectListItem
{
Value = m.PODObjectID,
Text = m.name
});
ViewBag.PODObjectChoices = podObjectChoices.ToList();
return View();
In your view
#Html.DropDownListFor(m => m.PODObjectID, (IEnumerable<SelectListItem>)ViewBag.PODOBjectChoices, new { #class = "control-label col-md-2" })
I pretty much just covered this in the sample code above, but you can't directly set PODOBject via a drop down list. Instead, you set the foreign key property for the relationship, PODOBjectID, and Entity Framework will wire it up.
Hope someone can help - this has been bugging me for around 2 hours - its probably something simple :)
Kendo UI Grid sends a request to my controller
http://localhost:1418/user/update?UserID=1&UserName=Admin&RoleName=Admin&Email=c.j.hannon%40gmail.com&Active=true&Company%5BCompanyID%5D=1&Company%5BCompanyName%5D=asd
However, the controller class 'Company' isnt bound by the binder? Can any one help my view model and controller action signature are below:
[HttpGet]
public JsonResult Update(UserViewModel model)
{
svcUser.UpdateUser(new UpdateUserRequest() {
UserID=model.UserID,
RoleID = model.RoleName,
Email = model.Email,
Active = model.Active.GetValueOrDefault(false),
UserName = model.UserName
});
return Json("", JsonRequestBehavior.AllowGet);
}
public class UserViewModel
{
public int UserID { get; set; }
public string UserName { get; set; }
public string RoleName { get; set; }
public string Email { get; set; }
public bool? Active { get; set; }
public CompanyViewModel Company { get; set; }
}
Cheers
Craig
A few things. Your immediate problem is that Company is mapped to a complex object not a primitive type. Kendo Grid just does not do this (as of this writing). Just guessing, but you probably want to setup a foreign key binding on the Grid and just pass back the Id of the company from a listbox. This is not as bad as you think and it will immediatly fix your problem and look nice too.
Maybe personal taste but seems to be a convention. Use the suffix ViewModel for the model that is bound to your View and just the suffix Model for your business objects. So a Kendo Grid is always populated with a Model.
Ex.:
public class UserModel
{
public int UserID { get; set; }
public string UserName { get; set; }
public string RoleName { get; set; }
public string Email { get; set; }
public bool? Active { get; set; }
public int CompanyID { get; set; }
}
public class CompanyModel
{
public int ID { get; set; }
public string Name { get; set; }
}
public class UserViewModel
{
public UserModel UserModel { get; set; }
public IList<CompanyModel> Companies { get; set; }
}
public ActionResult UserEdit(string id)
{
var model = new UserViewModel();
model.UserModel = load...
model.Companies = load list...
return View(model);
}
#model UserViewModel
...
column.ForeignKey(fk => fk.CompanyId, Model.Companies, "ID", "Name")
(Razor Notation)
BUT! This is just an example, you are better off Ajax loading the Grid with the IList becuase I assume you have many Users in the Grid at once, though you could server bind off the ViewModel with a List too. But the list of Companies is probably the same every time, so map it to the View just liek this rather than Ajax load it every time you do a row edit. (not always true)
I have the following 2 entities:
public class Product
{
[Key]
public int ID { get; set; }
[Required]
public string Name { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
[Key]
public int ID { get; set; }
[Required]
public string Name { get; set; }
public ICollection<Product> Products { get; set; }
}
and a view model
public class ProductCreateOrEditViewModel
{
public Product Product { get; set; }
public IEnumerable<Category> Categories { get; set; }
}
The create view for Product uses this ViewModel. The category ID is set as follows in the view:
<div class="editor-field">
#Html.DropDownListFor(model => model.Product.Category.ID,new SelectList
(Model.Categories,"ID","Name"))
#Html.ValidationMessageFor(model => model.Product.Category.ID)
</div>
When the form posts I get an instance of the view model with a product and the selected category object set but since the "Name" property of Category has a [Required] attribute the ModelState is not valid.
As far as creating a Product goes I don't need or care for the "Name" property. How can I get model binding to work such that this is not reported as a ModelState error?
You should create a correct ViewModel for your View.
The best approach imo is not to expose your domain entities to the view.
You should do a simple DTO flattening from your entities to your viewmodel.
A class like that
public class ProductViewModel
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
public int CategoryId? { get; set; }
public SelectList Categories { get; set; }
}
From your controller you map the product to your viewmodel
public ViewResult MyAction(int id)
{
Product model = repository.Get(id);
//check if not null etc. etc.
var viewModel = new ProductViewModel();
viewModel.Name = model.Name;
viewModel.CategoryId = model.Category.Id;
viewModel.Categories = new SelectList(categoriesRepo.GetAll(), "Id", "Name", viewModel.CategoryId)
return View(viewModel);
}
Then in the action that respond to the post, you map back your viewModel to the product
[HttpPost]
public ViewResult MyAction(ProductViewModel viewModel)
{
//do the inverse mapping and save the product
}
I hope you get the idea
After researching all day and night, I have something that is currently working. However, I am not sure I really understand what's going on with navigation properties and entity relationships, so I'm concerned that my code might cause problems in the future. I had to manually set the navigation properties to "EntityState.Modified". My model may eventually have many levels of navigation objects and collections. Is there an easier way to update the related entities? If there is no easier way, is this approach okay?
Here is the view model
public class ViewModel {
public ViewModel() { }
public ViewModel(Context context) {
this.Options = new SelectList(context.Options, "Id", "Name");
}
public Parent Parent { get; set; }
public SelectList Options { get; set; }
}
entity classes
public class Parent {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ChildOne ChildOne { get; set; }
public virtual ChildTwo ChildTwo { get; set; }
}
public class ChildOne {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Parent Parent { get; set; }
public virtual int OptionId { get; set; }
public virtual Option Option { get; set; }
}
public class ChildTwo {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Parent Parent { get; set; }
public virtual int OptionId { get; set; }
public virtual Option Option { get; set; }
}
public class Option {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ChildOne> ChildrenOnes { get; set; }
public virtual ICollection<ChildTwo> ChildrenTwos { get; set; }
}
context
public class Context : DbContext {
public DbSet<Parent> Parents { get; set; }
public DbSet<ChildOne> ChildrenOnes { get; set; }
public DbSet<ChildTwo> ChildrenTwos { get; set; }
public DbSet<Option> Options { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Parent>()
.HasOptional(x => x.ChildOne)
.WithOptionalPrincipal(x => x.Parent);
modelBuilder.Entity<Parent>()
.HasOptional(x => x.ChildTwo)
.WithOptionalPrincipal(x => x.Parent);
}
}
controller
private Context db = new Context();
public ActionResult Edit() {
ViewModel viewmodel = new ViewModel(db);
viewmodel.Parent = db.Parents.Find(1);
return View(viewmodel);
}
public void Save(Parent parent) {
if (ModelState.IsValid) {
db.Entry(parent).State = EntityState.Modified;
db.Entry(parent.ChildOne).State = EntityState.Modified;
db.Entry(parent.ChildTwo).State = EntityState.Modified;
db.SaveChanges();
}
}
and view
#model MvcApp7.Models.ViewModel
<div id="Parent">
#Html.HiddenFor(model => model.Parent.Id)
#Html.TextBoxFor(model => model.Parent.Name)
<div id="ChildOne">
#Html.HiddenFor(model => model.Parent.ChildOne.Id)
#Html.TextBoxFor(model => model.Parent.ChildOne.Name)
#Html.DropDownListFor(model => model.Parent.ChildOne.OptionId, Model.Options)
</div>
<div id="ChildTwo">
#Html.HiddenFor(model => model.Parent.ChildTwo.Id)
#Html.TextBoxFor(model => model.Parent.ChildTwo.Name)
#Html.DropDownListFor(model => model.Parent.ChildTwo.OptionId, Model.Options)
</div>
</div>
<input id="SaveButton" type="button" value="save" />
<script type="text/javascript">
$('#SaveButton').click(function () {
var data = $('input, select, textarea').serialize();
$.post('#Url.Action("Save")', data, function () { });
});
</script>
Yes you are doing it right. When working with detached entities like in web application you must tell EF exactly what state each entity has. In your scenario you will first call:
db.Entry(parent).State = EntityState.Modified;
In EFv4.1 this operation causes that parent is attached to context. Only attached entities can be persisted to database when SaveChanges is called. The state of the entity is set to modified so context will try to update existing record in the database when persisting the entity. There is one more important thing which happened when you called that statement: All related entities are attached as well but their state is set to unchanged. You must manually set correct state of all related entities because EF doesn't know which one is new, modified or deleted. That is why your following calls are correct as well.
Edit:
Be aware that this updates values in parent and child but it doesn't update relation itself. If you swap one child with another the process is much more complicated when using independent associations.