MVC 4 binding to List for child table - c#

I have a Product model created and would like to add relevant taxes for the product from a Tax model. A single product can have multiple taxes applied so I have created another model TaxApplied to store relations.
I have added a ListBox with MultiSelectList to the Create view for Product which shows the available taxes.
#Html.ListBox("AppliedTaxes", ViewBag.AppliedTaxes as MultiSelectList)
But I get the following error when I try to create a product with selected Taxes. How should I modify the view,model or controller such as to add the tax relations as well?
The ViewData item that has the key 'AppliedTaxes' is of type 'System.Collections.Generic.List`1[[StoreManager.Models.TaxApplied, StoreManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' but must be of type 'IEnumerable<SelectListItem>'.
Models Described below
public class Product
{
public int ID { get; set; }
public string Name { get; set;
public float SalePrice { get; set; }
public List<TaxApplied> AppliedTaxes { get; set; }
}
public class Tax
{
public int ID { get; set; }
[DisplayName("Tax Code")]
public string TaxCode { get; set; }
[DisplayName("Tax Percent")]
public string Value { get; set; }
}
public class TaxApplied
{
public int ID { get; set; }
[ForeignKey("Product")]
public int ProductID { get; set; }
[ForeignKey("Tax")]
public int TaxID { get; set; }
public virtual Product Product { get; set; }
public virtual Tax Tax { get; set; }
}
Controller actions for Product create
//
// GET: /Product/Create
public ActionResult Create()
{
ViewBag.NavProduct = "active";
MultiSelectList taxes = new MultiSelectList(db.Taxes.ToList<Tax>(), "ID", "Name");
ViewBag.AppliedTaxes = taxes;
return View();
}
//
// POST: /Product/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Product product)
{
ViewBag.NavProduct = "active";
if (ModelState.IsValid)
{
db.Products.Add(product);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(product);
}

You need to return a MultiSelectList from your generic list.
Here is an example of how to do it:
var appliedTaxes = new List<TaxApplied>();
appliedTaxes.Add(new TaxApplied { ID = 1, ProductID = 1 });
var items = appliedTaxes.Select(t => new SelectListItem { Text = t.ID.ToString(), Value = t.ProductID.ToString() }).ToList();
ViewBag.AppliedTaxes = new MultiSelectList(items, "Text", "Value");
Please note I am only using the Id's as the text and value for demonstration purposes.
Update
Change your post to this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Product product)
{
ViewBag.NavProduct = "active";
if (ModelState.IsValid)
{
db.Products.Add(product);
db.SaveChanges();
return RedirectToAction("Index");
}
MultiSelectList taxes = new MultiSelectList(db.Taxes.ToList<Tax>(), "ID", "Name");
ViewBag.AppliedTaxes = taxes;
return View()
}

Related

How to display data from two separate tables in MVC?

So, i am new to programming and i am trying to make my first .net project, and i'm stuck.
I have a database that contains table Product, it looks like this:
[Key]
public int ProductId { get; set; }
public string ProductName { get; set; }
[Range(1, int.MaxValue)]
public double ProductPrice { get; set; }
public string ProductDescription { get; set; }
public string ProductImagePath { get; set; }
public int ProductColorId { get; set; }
[ForeignKey("ProductColorId")]
public virtual ProductColor ProductColor { get; set; }
public int ProductShippingOptionId { get; set; }
[ForeignKey("ProductShippingOptionId")]
public virtual ProductShippingOption ProductShippingOption { get; set; }
public int ProductConditionId { get; set; }
[ForeignKey("ProductConditionId")]
public virtual ProductCondition ProductCondition { get; set; }
Columns ProductShippingOption, ProductColor and ProductCondition are a separate tables that each contain columns for Id and Name.
When i add a product to database, i want to show details of just one product in a view, but i need to display ProductConditionName instead of ProductConditionId (for example).
What should i include in my ViewModel and my Controller so i can use it in my View?
My action in a ProductController looks like this
public IActionResult ProductTemplate(int? id)
{
ProductVM productVM = new ProductVM()
{
Product = _db.Product.Find(id),
};
if (id == 0)
{
return NotFound();
}
if (id == 0)
{
return NotFound();
}
if(productVM==null)
{
return NotFound();
}
return View(productVM);
}
Thanks!
Best and easiest way to do it is including the classes in Product:
For your ProductTemplate action :
ProductVM productVM = new ProductVM()
{
Product = _db.Product.Where(s=>s.Id == id)
.Include(s=>s.ProductColor)
.Include(s=>s.ProductShippingOption)
.Include(s=>s.ProductCondition)
.FirstOrDefault();
};
And you can call them in your .cshtml with (Let say you want ProductColor name) :
#Model.Product.ProductColor.Name
Alternatively you can add Include() to your context to take all includes defaultly.

.NET List not saved to db?

I want an item to have several sales linked to it:
Sale.cs
public class Sale
{
public int Id { get; set; }
public int Amount { get; set; }
public decimal Price { get; set; }
}
Item.cs
public class Item
{
[Key]
public string Name { get; set; }
public string Market { get; set; }
public string Market_api { get; set; }
public List<Sale> Sales { get; set; }
}
I save Sales like this:
[HttpPost]
public async Task<IActionResult> Sale(SaleViewModel vm, string name)
{
Item item = _repo.GetItem(name);
item.Sales = item.Sales ?? new List<Sale>();
item.Sales.Add(new Sale
{
Amount = vm.Amount,
Price = vm.Price
});
_repo.UpdateItem(item);
await _repo.SaveChangesAsync();
return RedirectToAction("Index");
}
_repo:
public void UpdateItem(Item item)
{
_ctx.Items.Update(item);
}
public async Task<bool> SaveChangesAsync()
{
if(await _ctx.SaveChangesAsync() > 0)
{
return true;
}
return false;
}
and when I debug this it all looks good (List is in item)
Debug information
Peek into db
but when I try to access it like:
item.Sales it always returns null. I honestly don't know what's going on, I can see that the correct foreign key is saved in the Sales table but as to why I can't access it I have no clue.
Include Sales in your repo get items method
_repo.GetItem(name);
_ctx.Items.Include(i => i.Sales).FirstOrDefault(i => i.Name == name);

MVC - Validation (Model.State) using ViewModel

I´m needing some help. I have the following scenario and I think that I doing something wrong.
- Model "State"
namespace App.Model
{
public class State
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int idState { get; set; }
[Required(ErrorMessage = "Initials is required")]
public string StateInitials { get; set; }
[Required(ErrorMessage = "Name is required")]
public string StateName { get; set; }
[Display(Name = "Update Date")]
[DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime? UpdateDate { get; set; }
[Display(Name = "Update Responsible")]
public string UpdateResponsible { get; set; }
} //class
} //namespace
- Model "Location"
namespace App.Model
{
public class Location
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int idLocation { get; set; }
public int idState { get; set; }
[Required(ErrorMessage = "Name is required")]
public string LocationName { get; set; }
public string Status { get; set; }
public string ManagerName { get; set; }
public string Address { get; set; }
[Display(Name = "Update Date")]
[DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime? UpdateDate { get; set; }
[Display(Name = "Update Responsible")]
public string UpdateResponsible { get; set; }
} //class
} //namespace
The relation between State and Location is one to many, but I didn´t describe this on model (using navigation fields).
I have a view where I want to edit the locations. To do that, I´m using the following view model.
- View Model "LocationsViewModel"
namespace App.ViewModel
{
public class LocationsViewModel
{
public State objState { get; set; }
public List<Location> lstLocations { get; set; }
} //class
} //namespace
To edit the Locations I use the following controller.
namespace App.Controllers
{
public class LocationController : Controller
{
private DbContext db = new DbContext();
// GET: /Location/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
State objState = db.States.Find(id);
if (objState == null)
{
return HttpNotFound();
}
LocationsViewModel model = new LocationsViewModel();
model.objState = objState;
model.lstLocations = getLocations(objState.idState); //I didn´t show this method here just to simplify
return View(model);
} //Edit
// POST: /Location/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Editar(LocationsViewModel model)
{
State objState = db.States.Find(model.objState.idState);
try
{
if (ModelState.IsValid)
{
//Saving Locations
foreach (Location obj in model.lstLocations)
{
Location objLocation = db.Locations.Find(obj.idLocation);
objLocation.LocationName = obj.LocationName;
objLocation.Status = obj.Status;
objLocation.ManagerName = obj.ManagerName;
objLocation.Address = obj.Address;
objLocation.UpdateDate = DateTime.Now;
objLocation.UpdateResponsible = User.Identity.Name;
db.Entry(objLocation).State = EntityState.Modified;
db.SaveChanges();
} //foreach
return RedirectToAction("Index");
}
}
catch (Exception e)
{
ModelState.AddModelError("", e.Message);
}
model.objState = objState;
model.lstLocations = getLocations(objState.idState); //I didn´t show this method here just to simplify
return View(model);
} //Edit
} //class
} //namespace
The problem/question is:
I wrote this code to edit (save) the list of locations of a specific State. When I submit the "Edit" view, the MVC try to validade the list of Locations (lstLocations) and the State (objState) as well, but I want to validate only the list of locations.
Note 1. I need to pass to my Edit view both objects: objState and lstLocations. I need the objState object because I show some State´s properties to the user on page (view).
Note 2. I´m getting ModelState.IsValid = false because model.objLocation is not valid, but I don´t want to check objLocation (is not relevant for this view). I just want to check the list of Locations (lstLocation)
What is the best approach to achieve my gol? Am I doing something wrong? Need I to change my way of thinking?
You'll need two things. The first is to remove the object from the ModelState which you do not want to validate. The second is you need to put your code block that is to execute in a valid state within the if(ModelState.IsValid) block.
public ActionResult Edit(int? id)
{
//don't validate this field
ModelState.Remove("yourObject.property");
if (ModelState.IsValid)
{
...
}
}
In addition to the solution you've already chosen, I found the custom RequiredIfAttribute useful. Using that, you can control if something is required base upon another condition, e.g.:
...
public bool RequireLocationName {
get {
return !Addresses.Any();
}
}
[RequiredIf("RequireLocationName", true)]
public bool LocationName { get; set; }

Entity Framework & ASP.NET MVC more complex models

I have Entity Framework model which contains reference to other Entity like
public class Product
{
[Key]
public int ProductID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual Shop Shop { get; set; }
[Required]
public double Price { get; set; }
}
I would like to create Edit View which contain Shop selector (DropDown).
By default I have created basic MVC Controller with Entity model connected, which created Edit like:
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Product product = db.Products.Find(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
and View does not contain Shop selector.
I have tried to add DropDown like:
#Html.DropDownListFor(product => product.Shop, (SelectList)ViewBag.Shops)
But in POST method, Shop entity is null.
How to handle that?
Create a view model to represent what you want to display
public class ProductVM
{
public int ProductID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public int? ShopID { get; set; }
[Required]
public double Price { get; set; }
public SelectList ShopList { get; set; }
}
and in your controller, map your model to the view model
public ActionResult Edit(int? ID)
{
....
Product product = db.Products.Find(id);
ProductVM model = new ProductVM();
// map properties
....
// populate select list (assumes Shop has properties ID and Name)
model.ShopList = new SelectList(db.Shops, "ID", "Name");
return View(product);
}
and in your view
#model ProductVM
....
#Html.DropDownListFor(m => m.ShopID, Model.ShopList, "--Select shop--")
#Html.ValidationMessageFor(m -> m.ShopID)
this will post back the model with the selected ID of the Shop. Select controls post back single values so you cannot post back a complex object such as Shop. The POST method would be
[HttpPost]
public ActionResult Edit(ProductVM model)
{
....
}
Note you can use tools such as automapper to make mapping easier
I hope this helps.
Model for Product:
public class Product
{
public int ProductID { get; set; }
public string Name { get; set; }
public int ShopID { get; set; }
public double Price { get; set; }
}
Then a ViewModel for Product:
public class ProductViewModel
{
public Product Model { get; set; }
public IEnumerable<SelectListItem> Shops{ get; set; }
public ProductViewModel()
{
GetShops();
}
public void GetShops()
{
Shops = new List<SelectListItem>();
var collectionShops = GetShopsFromDatabase();
Shops.AddRange(
collectionShops.Select(
contract =>
new SelectListItem
{
Text = contract.ShopDescription,
Value = contract.ShopID.ToString()
}));
}
}
In your View:
#model ProductViewModel
....
#Html.DropDownListFor(x => x.Model.ShopID, Model.Shops, new { #title = "Please select a shop" })

Scaffolding ModelView creates underlying database tables

I'm trying to use ViewModels for the first time using AutoMapper. I have two models:
public class Item
{
public int ItemId { get; set; }
public bool Active { get; set; }
public string ItemCode { get; set; }
public string Name { get; set; }
public List<ItemOption> ItemOptions { get; set; }
//...
}
public class ItemOption
{
public int ItemOptionId { get; set; }
public string Name { get; set; }
public string Barcode { get; set; }
//...
}
Which I have turned into two ViewModels:
public class ItemDetailViewModel
{
public int ItemDetailViewModelId { get; set; }
public int ItemId { get; set; }
public bool Active { get; set; }
public string ItemCode { get; set; }
public string Name { get; set; }
public List<ItemDetailItemOptionViewModel> ItemOptions { get; set; }
}
public class ItemDetailItemOptionViewModel
{
public int ItemDetailItemOptionViewModelId { get; set; }
public int ItemOptionId { get; set; }
public string Name { get; set; }
public string Barcode { get; set; }
}
I then set the following in my application start-up:
Mapper.CreateMap<Item, ItemDetailViewModel>();
Mapper.CreateMap<ItemOption, ItemDetailItemOptionViewModel>();
Finally I scaffolded my ItemDetailViewModel:
I then built my project and added a new Item through /Item/Create
I had a look in the database expecting to see that I would have an entry in the Item table, but instead I have ItemDetailViewModel and ItemDetailItemOptionViewModel tables, which I wasn't expecting and the data is is ItemDetailViewModel.
I assume I have done something wrong with my scaffolding? How do I scaffold off the ViewModel without making it part of the main business models?
Further Details
If it isn't possible to scaffold the controller with a ViewModel, then how do I reference the ViewModel in the controller and save changes back to the database?
For example what would the following change to once I remove ItemDetailViewModel from the db context?
//
// POST: /Item/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ItemDetailViewModel itemdetailviewmodel)
{
if (ModelState.IsValid)
{
db.ItemDetailViewModels.Add(itemdetailviewmodel);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(itemdetailviewmodel);
}
Further Details [2]
So am I correct that my Index/Details should work as so or is there a better way of doing it?
//
// GET: /Item/
public ActionResult Index()
{
var items = db.Items.ToList();
var itemdetailviewmodel = AutoMapper.Mapper.Map<ItemDetailViewModel>(items);
return View(itemdetailviewmodel);
}
//
// GET: /Item/Details/5
public ActionResult Details(int id = 0)
{
ItemDetailViewModel itemdetailviewmodel = AutoMapper.Mapper.Map<ItemDetailViewModel>(db.Items.Find(id));
if (itemdetailviewmodel == null)
{
return HttpNotFound();
}
return View(itemdetailviewmodel);
}
Scaffolding is not that intelligent. The standard controller scaffolding template is creating a DbContext with the controller model and presumes you are working with the DB models, not view models and it does not use Automapper. So you'll need to either not use scaffolding, or check what it has done before using it.
And nothing is wrong with the way you use scaffolding, it is just not supposed to do what you expect.
Update this is how you do this without scaffolding
// Without Automapper
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ItemDetailViewModel model)
{
if (ModelState.IsValid)
{
var item = new Item()
{
Active = model.Active,
ItemCode = model.ItemCode,
Name = model.Name,
ItemOptions = // code to convert from List<ItemDetailItemOptionViewModel> to List<ItemOption>
}
db.Items.Add(item);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(model);
}
// with Automapper - not recommended by author of Automapper
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ItemDetailViewModel model)
{
if (ModelState.IsValid)
{
var item = Automapper.Mapper.Map<Item>(model);
db.Items.Add(item);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(model);
}
You'll need to modify your DbContext to have IDbSet<Item> Items instead of IDbSet<ItemDetailViewModels> ItemDetailViewModels.
Automapper was created to map from Domain Models to View Models and not the other way. I have done that for a while, but this is troublesome and causes subtle bugs and other maintenance problems. Even Jimmy Bogard himself says you should not map from view models to domain models.

Categories