EF not saving virtual properties when editing - c#

I have a model Team which looks like this:
public class Team
{
[Key]
public int TeamId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public virtual Group Group { get; set; }
}
I have a method to Edit it:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Team team)
{
using (var db = new DbConnection())
{
if (ModelState.IsValid)
{
db.Entry(team).State = EntityState.Modified;
db.Groups.Attach(team.Group);
db.SaveChanges();
}
return PartialView(team);
}
}
However it is not saving the change to the Group column. Meanwhile Create method does work although I see no difference between them:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Team team)
{
using (var db = new DbConnection())
{
if (ModelState.IsValid)
{
db.Groups.Attach(team.Group);
db.Teams.Add(team);
db.SaveChanges();
return RedirectToAction("Index");
}
return PartialView(team);
}
}
The values for the Group come the same in both methods. I bring the values to the form like this (again, same thing for both):
ViewBag.Groups = db.Groups.ToList().Select(g => new SelectListItem()
{
Text = g.Code,
Value = g.GroupId.ToString(),
Selected = false
}).ToList();
and use them like this:
#Html.DropDownListFor(model => model.Group.GroupId, (List<SelectListItem>)ViewBag.Groups)
Can someone please explain what the difference between the two methods is (in terms of one saving Group and other - not)?
P.S. Using both the latest MVC and EF.

Apparently it is a must (?) (at least I couldn't find anything better) to add a field matching the type of the Group in order to be able to easily save. No need to attach anything this way. Now my model looks like:
public class Team
{
[Key]
public int TeamId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public int? GroupId { get; set; }
public virtual Group Group { get; set; }
}
and my Save and Create methods got this line:
db.Groups.Attach(team.Group);
removed.

Related

Persisting unbound viewmodel data when posting

I am currently in the process of getting accustomed to MVC, having come from ASP.Net.
So far I have found ways to achieve what I want to do, but with this one I am getting a "This cannot be the easiest way" moment.
Scenario:
I am migrating a quoting application to MVC that has an existing database so my model classes are auto-generated. I have created a viewmodel class for each controller action that needs to display data to the user.
The edit quote viewmodel looks like this:
public class QuoteEdit_ViewModel
{
public SelectList DelLocations { get; set; }
public int QuoteID { get; set; }
public string QuoteNo { get; set; }
public string EnquiryNo { get; set; }
public string SalesPerson { get; set; }
public string Exceptions { get; set; }
public string CreatedBy { get; set; }
public string ModifiedBy { get; set; }
[Required]
[Display(Name = "Equipment Overview")]
public string EquipmentOverview { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
[Required]
public int? Validity { get; set; }
[Required]
[Display(Name = "Minimum Delivery Weeks")]
public int? DeliveryMin { get; set; }
[Required]
[Display(Name = "Maximum Delivery Weeks")]
public int? DeliveryMax { get; set; }
[Required]
[Display(Name = "Delivery Location")]
public int? DelLocationID { get; set; }
public List<Constants.QPT> PackTypes { get; set; }
public List<Constants.QE> Equipments { get; set; }
public List<Constants.QEEx> Extras { get; set; }
}
The lists at the bottom contain equipment lines etc that are junction tables in the database.
Currently I can edit this and post the data back to the database and it works perfectly.
The part that seems messy is the following, specifically the part after the if:
public ActionResult Save(QuoteEdit_ViewModel VM)
{
Quote a = DAL.DB.Quotes.Where(x => x.QuoteID == VM.QuoteID).Single();
TryUpdateModel(a);
if (ModelState.IsValid)
{
DAL.DB.SaveChanges();
return RedirectToAction("Dashboard", "Home");
}
VM.DelLocations = DAL.GetDeliveryLocationDropdown();
var QData = DAL.GetQuoteEditVM(VM.QuoteID);
VM.QuoteNo = QData.QuoteNo;
VM.EnquiryNo = QData.EnquiryNo;
VM.SalesPerson = QData.SalesPerson;
VM.PackTypes = QData.PackTypes;
VM.Equipments = QData.Equipments;
VM.Extras = QData.Extras;
VM.Created = QData.Created;
VM.CreatedBy = QData.CreatedBy;
VM.Modified = QData.Modified;
VM.ModifiedBy = QData.ModifiedBy;
return View("Edit", VM);
}
Currently I need to reload the entire viewmodel and repopulate any fields that were not bound in the view as their values are lost during the POST.
I have read in other posts that you can use hiddenfor, but can this be used for Lists as well?
Also is this the correct way to approach this or am I completely missing the point of MVC?
Do not use HiddenFor. You're going about the correct way. The only change I would make is factoring out your common code into another method(s) that both the GET and POST actions can utilize.
private void PopulateQuoteEditViewModel(QuoteEdit_ViewModel model)
{
mode.DelLocations = DAL.GetDeliveryLocationDropdown();
var QData = DAL.GetQuoteEditVM(model.QuoteID);
model.QuoteNo = QData.QuoteNo;
model.EnquiryNo = QData.EnquiryNo;
model.SalesPerson = QData.SalesPerson;
model.PackTypes = QData.PackTypes;
model.Equipments = QData.Equipments;
model.Extras = QData.Extras;
model.Created = QData.Created;
model.CreatedBy = QData.CreatedBy;
model.Modified = QData.Modified;
model.ModifiedBy = QData.ModifiedBy;
}
Then:
public ActionResult QuoteEdit()
{
var model = new QuoteEdit_ViewModel();
PopulateQuoteEditViewModel(model);
return View(model);
}
[HttpPost]
public ActionResult QuoteEdit(QuoteEdit_ViewModel model)
{
if (ModelState.IsValid)
{
...
}
PopulateQuoteEditViewModel(model);
return View(model);
}
Other Comments
Don't use TryUpdateModel. It's not meant to be used the way you are here. The correct approach is to map over the posted values from your view model. You can either do this manually, or utilize a library like AutoMapper. Either way, you don't want to just willy-nilly overwrite anything on your database entity based on raw posted data as you're doing.
You should never post the ID of the entity you're editing, but rather rely on obtaining it from the URL via a route param. If the URL is changed to a different ID, you are literally editing a different thing, and you can add object-level permissions and such to control who can edit what. However, the ID that's posted can be manipulated, and if you aren't careful (as you aren't being here), then a user can tamper with the ID to mess with objects they potentially shouldn't be editing.

How to use ViewModels in Edit Action without making another request?

I need a specific answer or solution about this particular case ,
I have EditVehicleViewModel that's passed to Edit Vehicle Controller Action like so
public async Task<ActionResult> Edit(EditVehicleViewModel vehiclViewModel)
{
if (ModelState.IsValid)
{
// I NEED TO MAP THE VIEW_MODEL TO THE MODEL HERE
db.Entry(vehicleModel).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(vehiclViewModel);
}
I need to update the Model based on the changes that's been to the ViewModel , without making a request to get the Vehicle that's changed and SaveChanges() on it , while still using this line of code if possible
db.Entry(vehiclViewModel).State = EntityState.Modified;
EDIT
Here's my model and ViewModel
ViewModel
public class EditVehicleViewModel
{
[Required]
public string LicenceNumber { get; set; }
public int? Year { get; set; }
public string Color { get; set; }
[DataType(DataType.Upload)]
public HttpPostedFileBase ImageUpload { get; set; }
public string VINNumber { get; set; }
}
Model
public class Vehicle
{
public int ID { get; set; }
[Required]
public string LicenceNumber { get; set; }
[Required]
public Nullable<DateTime> Year { get; set; }
[Required]
public string Color { get; set; }
[DataType(DataType.ImageUrl)]
public string ImageUrl { get; set; }
[Required]
public string VINNumber { get; set; }
}
First you have to install any one of the following package from nuget,
PM> Install-Package TinyMapper
OR
PM> Install-Package AutoMapper
then add in your code, if tinymapper is used
TinyMapper.Bind<Vehicle, EditVehicleViewModel>();
Vehicle vehicleModel = TinyMapper.Map<EditVehicleViewModel>(vehiclViewModel);
db.Entry(vehicleModel).State = EntityState.Modified;
OR
if automapper is used,
Mapper.CreateMap<Vehicle , EditVehicleViewModel>();
Vehicle vehicleModel = Mapper.Map<EditVehicleViewModel>(vehiclViewModel);
db.Entry(vehicleModel).State = EntityState.Modified;
Sorry if this is not right . I am still not too sure where vehicleModel is
public async Task<ActionResult> Edit(EditVehicleViewModel vehiclViewModel)
{
if (ModelState.IsValid)
{
// I am guessing the VINNumber is the identifier
Vehicle vModel = db.Vehicle.FirstOrDefault(v => v.VINNumber == vehiclViewModel.VINNumber);
// Mapping here
vModel.LicenceNumber = vehiclViewModel.LicenceNumber;
vModel.Year = vehiclViewModel.Year;
vModel.Color = vehiclViewModel.Color;
vModel.VINNumber = vehiclViewModel.VINNumber
vModel.ImageUrl = vehiclViewModel.ImageUrl;
db.Entry(vModel).State = vModel.ID == 0 ? EntityState.Added : EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(vehiclViewModel);
}
I assume this is a post because it is an edit. I might be wrong but it might be an idea to add the vehicle id that was used to identify it on the edit page load in a
#Html.Hidden()
That can then be used as the identifier when looking for the vehicle that needs to be saved

ASP.NET MVC and Mongodb references

I have problem with my application. In my simple blog application I have Post and Category collection:
Post.cs
public class Post
{
[ScaffoldColumn(false)]
[BsonId]
public ObjectId PostId { get; set; }
[ScaffoldColumn(false)]
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime Date { get; set; }
[Required]
public string Title { get; set; }
[ScaffoldColumn(false)]
public string Url { get; set; }
public ObjectId CategoryId { get; set; }
[UIHint("WYSIWYG")]
[AllowHtml]
public string Details { get; set; }
[ScaffoldColumn(false)]
public string Author { get; set; }
[ScaffoldColumn(false)]
public int TotalComments { get; set; }
[ScaffoldColumn(false)]
public IList<Comment> Comments { get; set; }
}
and category.cs
public class Category
{
[ScaffoldColumn(false)]
[BsonId]
public ObjectId CategoryId { get; set; }
[ScaffoldColumn(true)]
[Required]
public string Name { get; set; }
}
PostControler:
[HttpPost]
public ActionResult Create(Post post)
{
if (ModelState.IsValid)
{
post.Url = post.Title.GenerateSlug();
post.Author = User.Identity.Name;
post.Date = DateTime.Now;
post.CategoryId =
_postService.Create(post);
return RedirectToAction("Index");
}
return View();
}
When I create new post, I want to choose category from list and save "_id" of category in post collection. I don't have any idea to resolve this. Can anybody give some suggestions, to solve this.
I don't really understand the problem here, but let's walk through this step-by-step:
First, you'll have to populate the list of available categories, so you'll have to call FindAll on your Category collection.
Second, you will need to pass this list to the frontend, so you need a view model class that contains some kind of dictionary so you can bind this to a dropdown (<select>), for instance, i.e.
class PostCreateViewModel
{
Dictionary<string, string> Categories {get; set;}
/// ...
}
[HttpGet]
public ActionResult Create()
{
var categories = _categoryService.All();
var vm = new PostCreateViewModel();
vm.Categories = categories.ToDictionary(p => p.CategoryId.ToString());
// etc.
return View(vm);
}
Third, your Post handler should verify that the CategoryId is correct:
[HttpGet]
public ActionResult Create(Post post)
{
// Make sure the given category exists. It would be better to have a
// PostService with a Create action that checks this internally, e.g.
// when you add an API or a different controller creates new posts as
// a side effect, you don't want to copy that logic.
var category = _categoryService.SingleById(post.CategoryId);
if(category == null)
throw new Exception("Invalid Category!");
// Insert your post in the DB
// Observe the PRG pattern: Successful POSTs should always redirect:
return Redirect("foo");
}
Some details can be tricky, for instance I'm assuming that post.CategoryId will be populated through a model binder, but you might have to write a custom model binder for ObjectId or use a ViewModel class with a string and parse the string using ObjectId.Parse. In general, I'd suggest using ViewModels everywhere and use the help of something like AutoMapper to avoid writing tons of boilerplate copy code.

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.

Entity framework add collection to model not saving correctly

Maybe a simple question, but I can't seem to figure it out. Saving a collection to a model when adding a model to the database isn't working. I have a site which uses asp.net MVC and entity framework.
The models:
public class Event
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<EventRange> Ranges { get; set; }
}
public class EventRange
{
public int Id { get; set; }
public string RangeName { get; set; }
public string RangeDescription { get; set; }
public int Capacitiy { get; set; }
}
The controller actions:
[HttpPost]
public ActionResult Create(Event model)
{
ICollection<EventRange> eventRanges = new Collection<EventRange>();
var range = new EventRange {RangeName = "testrange", RangeDescription = "test", Capacitiy = 5}
eventRanges.Add(range);
model.Ranges = eventRanges;
db.Events.Add(model);
db.SaveChanges();
return View();
}
public ActionResult Events()
{
return View(db.Events);
}
When setting a breakpoint in the Events action and evaluated the query, the Range isn't saved to the event:
Code Screenshot
Note that that the database created for the eventrange model by EF does save the range:
EF DB Screenshot
Am I doing something wrong?
What if you mark the Ranges property as virtual?
public virtual ICollection<EventRange> Ranges { get; set; }

Categories