MVC and EF4 CTP model-binding and saving hierarchical model - asp.net-mvc

Am having trouble finding a clear answer to my situation when searching Stack Overflow and Google, hopefully someone can point me in the right direction.
My Situation
I want to be able to use a single edit form (in a single View) to update a 3-level-deep hierarchical entity using ASP.NET MVC 3 and Entity Framework 4 CTP (Code-first) - the model consists of Services, which can have many Service Options, which in Turn can have many Inventory Items.
I was expecting to be able to use MVCs default model binder (via TryUpdateModel) to:
Update an existing 'Service' record
Add/Update/Delete 'Service Option' records (attached to the Service) depending on posted values
Add/Update/Delete 'Inventory' records (attached to each Service Option) depending on posted values
My Model
[Bind(Include="Name, ServiceOptions")]
public class Service {
[Key]
public int ServiceID { get; set; }
public string Name { get; set; }
public DateTime DateCreated { get; set; }
public virtual ICollection<ServiceOption> ServiceOptions { get; set; }
}
[Bind(Include="ServiceOptionID, Description, Tags")]
public class ServiceOption {
[Key]
public int ServiceOptionID { get; set; }
public int ServiceID { get; set; } /* parent id reference */
public string Description { get; set; }
public virtual ICollection<Inventory> InventoryItems { get; set; }
}
[Bind(Include = "InventoryID, Description")]
public class Inventory {
[Key]
public int InventoryID { get; set; }
public int ServiceOptionID { get; set; } /* parent id reference */
public string Description { get; set; }
}
Ideal Controller Method:
[HttpPost]
public ActionResult EditService(int id) {
Service service = db.Services.Single(s => s.ServiceID == id);
TryUpdateModel(service); // automatically updates child and grandchild records
if (ModelState.IsValid) {
db.SaveChanges();
return RedirectToAction("Index");
}
return View(service);
}
Is there a way to achieve this utopian dream, or am I barking up the wrong tree? I'm open to using another technology (such as normal EF4, Automapper etc)
Thanks in advance!

With just the default model binder? Probably not.
With a custom one? Probably.
However your issue won't be the model binder itself. Your issue will be that EF and ORMs and ( I think ) in general do not consider removing an item from a collection as a delete operation. In effect what you are telling the ORM is the relationship does not exist, not that a child row needs to be deleted. Depending on your mappings you'll usually get an error like "A referential integrity constraint violation occurred". This won't be because of code first this is just how EF works.
EF works this way by design and is really important for more complex relationships such as when you have m2m relationships which reference other m2m relationships. You really want EF to be able to disambiguate calls for removal of a relationship and calls to remove a row entirely.
Also, IMHO, this technique is also bad because your letting the piece of code responsible for mapping http values also dictate how objects should be persisted. This is a bad move. I consider delete operations a pretty sacrosanct act and shouldn't be left to the ModelBinder alone. Without soft deletes or logging deleting objects should be considered "serious business".

Related

Getting an EntityType has no defined key error when trying to create a view from a MVC ViewModel

I'm working on an ASP.NET MVC 5 project in which I'm trying to build a controller from a MVC View Model. That ViewModel brings together 6 tables which I need to show in the views. Its my understanding that using MVC ViewModels is one way of showing multiple tables in a view. Anyway, I'm getting the following error message:
Error
There was an error running the selected code generator: 'Unable to retrieve metadata for
'PrismSmallTasks.ViewModels.ManageInterviewVM'. One of more validation errors were
detected during model generation:
ManageInterviewVM:: EntityType 'ManageInterviewVM' has no key defined.
Define the key for this EntityType.
ManageInterviewVMs: EntityType: EntitySet 'ManageInterviewsVMs' is based on
type 'ManageInterviewVM' that has no keys defined.
There's no key in the ManageInterviewVM ViewModel. That's because it's comprised of lists of the tables represented in the models that are in the VM. And each of those model classes do have a column that has a key defined for it.
For example, here's ManageInterviewVM:
public class ManageInterviewVM
{
public List<FieldRecord> FieldRecords { get; set; }
public List<TaskList> TaskLists { get; set; }
public List<InterviewARVTreatment> InterviewARVTreatments { get; set; }
public List<Note> Notes { get; set; }
public List<Risk> Risks { get; set; }
public List<Interview1> Interviews { get; set; }
}
And here's a partial listing of one of those tables as it is defined in the model class:
public partial class TaskList
{
[Key]
public int ID_TaskList { get; set; }
[Required]
[StringLength(15)]
public string CD_TaskListType { get; set; }
public int? ID_Profile { get; set; }
public int? ID_FieldRecord { get; set; }
public int? ID_Interview { get; set; }
So, I don't know what I'm missing. Why is this error showing up and how I can resolve it?
Your ViewModels should be completely dissociated from your Data Context (Data Access Layer). Only your domain models should deal with the DAL. ViewModels are only for displaying specific information to the view.
So after you create your ViewModel.. you try and create your view. When you arrive at this screen:
Type in your view name
Pick your template (if you leave it as 'Empty (without model)' then you should be able to just create it without any issue).
Once you pick a specific template and Model class (ViewModel), the 'Data context class' will auto-populate with your connection string (dbcontext), which is where your problem lies.
Since viewmodels are not supposed to be associated with the data access layer, you can just delete what is auto-populated in the 'Data context class' and then you should be able to create your view.
If you fall into the trap of thinking that you need to define keys for your viewmodel.. then your viewmodel class will be added to your connection string's class (dbcontext class).. which is a no-no.
You need to query the database using your domain models.. then you assign those values to your ViewModels properties that you want to display.
Hope this helps!

Populating a Lazy<T> collection

I am trying to get my head around lazy loading in an ASP.Net MVC app. For instance, I have a class with a property that is a collection (Employees). I want the collection to only load when I need it loaded:
public class Department
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
[ForeignKey("DepartmentId")]
public Lazy<ICollection<Employee>> Employees { get; set; }
}
First, I am not sure if I should lazy load the class or the collection
public Lazy<ICollection<Employee>> Employees { get; set; }
public ICollection<Lazy<Employee>> Employees { get; set; }
I assume the collection.
Next, I cannot seem to find a related example to actually load the property/collection once I need it and after the class has been instantiated. I am also not sure if this is done in the class itself or in my MVC controller.
Any help is appreciated.
You don't have to use the Lazy<T> on your Employees property. You'll just be adding unnecessary "lazyness", since Entity Framework (and other ORMs like NHibernate) queries are already lazy, i.e.: the query will only hit the database when you explicitly tell it to.
So, by making Employees of type:
public virtual ICollection<Employee> Employees { get; set; } //make sure to mark it as virtual, otherwise it won't be lazy
When querying:
var result = myContextObj.Departments.Include(d=> d.Employees).Where(d=> d.Id == someID).SelectMany(d=> d.Employees);
The code above does nothing but create a Query Object representing the query that may be sent to the database.But it's not going to do anything, unless you "materialize" the result, either by doing a foreach on result, or calling ToList() for example.
For lazy loading you must:
public virtual ICollection<Lazy<Employee>> Employees { get; set; }. You actually miss the virtual that allows the framework to create the proxy by overwritting the property;
context.Configuration.ProxyCreationEnabled = true;, this is the defautl value.

Retrieving related data using linq

I have an MVC app using EF code first. I add a user to the system and enter pension details, part of this is a dropdown linked to a model called PensionBenefitLevel. This is the model -
[Key]
public int PensionBenefitLevelID { get; set; }
public string DisplayText { get; set; }
public int EmployeePercentage { get; set; }
public int EmployerPercentage { get; set; }
public virtual ICollection<Pension> Pension { get; set; }
When registered I have the PensionBenefitLevelID that came from the dropdown, but in my controller I was to peform a calculation using the EmployerPercentage value that is related to that ID. Can anyone point me in the correct direction?
Do I need to create a variable in the controller and use a linq query to get that value back? I've not been able to find any examples of something similar so if you could point me to one that would be great too.
If I understand the question correctly, you want to get back the entity corresponding to PensionBenefitLevelID and perform a calculation on the EmployerPercentage field.
Since you haven't mentioned what pattern you are using with EF (repository, unit of work, etc.) I can only give you a general answer:
var entity = [Your DB Context].[Your Entity].GetById(pensionBenefitLevelID);
if(entity != null)
{
[Calculation]
}

Foreign key constraint, EF with collection of childobjects

I'm trying to update a model, but get the error "The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted."
From what I understand from The relationship could not be changed because one or more of the foreign-key properties is non-nullable the problem might be with how Entity Framework handles my virtual ICollection
However I'm not really sure how to implement the solution when using scaffolded repository pattern. Do I have to edit the Save()-method ParentObjectRepository-class?
Actually I really think that there must be some way to make EF understand this. I can't see how the EF-team was thinking "Probably noone is using a collection of objects with a foreign key constraint, lets not support that".
Update
Added code
[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection)
{
var eventRepository = new MagnetEventRepository();
var original = eventRepository.Find(id);
UpdateModel(original);
eventRepository.Save();
return RedirectToAction("Details", "Home", new { slug = original.Slug });
}
public void Save()
{
context.SaveChanges();
}
More code:
public class MagnetEvent
{
public virtual int Id { get; set; }
[Required]
public virtual string Name { get; set; }
[Required]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm}")]
[DataType(DataType.DateTime)]
public virtual DateTime? StartDate { get; set; }
public virtual string Description { get; set; }
[StringLength(100)]
public virtual string Slug { get; set; }
public virtual int MaximumCapacity { get; set; }
[DataType(DataType.Currency)]
public virtual int TicketPrice { get; set; }
public virtual int LocationId { get; set; }
public virtual Location Location { get; set; }
public virtual Collection<Ticket> Tickets { get; set; }
public virtual Collection<AttendeeInformationField> CaptureAttendeeInformationFields { get; set; }
public virtual int CustomerId { get; set; }
[Required]
public virtual CUSTOMER Customer { get; set; }
}
The Save()-method is from MagnetEventRepository, which is scaffolded from the above class.
Another update
I successfully removed the error by changing MagnetEventId in AttendeeInformationField to nullable int. When examining the database I can see exactly what's wrong.
Let's say I have a single AttendeeInformationField with the value "E-mail". When I edit my MagnetEvent, the AttendeeInformationField updates the MagnetEventId to null and then adds a new post with the correct MagnetEventId and Value.
I'd very much prefer if the posts in AttendeeInformationField were updated instead.
can you add the code for your event object. The one you call original.
It might be so that the UpdateModel change some info on the associated objects and that's not good if so. Not sure about this though I can't see all the code.
I prefer to not uder UptadeModel and instead use a inputmodel or your MVC model as the inparameter and manually map the chages to the loaded original object.
Antoher problem is that I can't see if
eventRepository.Save();
really do an SaveShages? does it? I can se some context code in another method Save?
As the exception say it seams like your associated collections or other associated objects cant find a valid ID value.
Are you Eager-loading the associated objects? like Customer?
One thing of note is that you shouldn't have the [Required] on Customer as its inferred from the fact that your FK isn't nullable. Required should only be used on a navigation property if you do not have the FK in the model.
To try to diagnose the issue, can you load the object and look at it in a debugger, you should expect that both locationId and CustomerId have non-zero values.
I found a solution to my problem. It seems to be a bug (?) in ASP.NET MVC when it comes to UpdateModel and a model containing an ICollection.
The solution is to override the default behaviour, as described in this blog post: http://www.codetuning.net/blog/post/Binding-Model-Graphs-with-ASPNETMVC.aspx
Update
I found a solution! The above only worked when updating existing items in the collection. To solve this, I have to manually check and add new AttendeeInformationFields. Like this:
[HttpPost]
public ActionResult Edit(int id, MagnetEvent magnetEvent)
{
var eventRepository = new MagnetEventRepository();
var original = eventRepository.Find(id);
UpdateModel(original);
foreach (var attendeeInformationField in magnetEvent.CaptureAttendeeInformationFields)
{
var attendeeInformationFieldId = attendeeInformationField.Id;
if (original.CaptureAttendeeInformationFields.AsQueryable().Where(ai => ai.Id == attendeeInformationFieldId).Count() == 0)
{
original.CaptureAttendeeInformationFields.Add(attendeeInformationField);
}
}
eventRepository.Save();
}
Together with the modified DefaultModelBinder, this actually works with both editing and adding. For now I haven't tried deleting.
Still, I hope there is a simpler way to do this. Seems like a lot of coding to do a very basic task.

Establish Foreign Key Connection Using Entity Framework With SQL Queries

I have a couple of classes (for this example anyway) that use code first with the entity framework to connect to the database.
public class Customer
{
[Key]
public long CustomerId { get; set; }
public string CompanyName { get; set; }
...
public virtual List<Contact> Contacts { get; set; }
}
public class Contact
{
[Key]
public long ContactId { get; set; }
public string Forename { get; set; }
...
public long CustomerId { get; set; }
public virtual Customer Customer { get; set; }
}
When I hook these up in my context class directly to the db the foreign key relationships hook up fine and I can access the collection of contacts from within the customer class.
class RemoteServerContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Contact> Contacts { get; set; }
...
}
My problem is that these database tables are used by various different systems and are massive. In order to increase efficiency I have overridden the default behaviour to point at a view (and also a stored proc elsewhere) rather than directly at the table.
public IEnumerable<Customer> Customers ()
{
return Database.SqlQuery<Customer>("SELECT * FROM vw_CustomerList");
}
public IEnumerable<Contact> Contacts()
{
return Database.SqlQuery<Contact>("SELECT * FROM vw_ContactsList");
}
I have made sure that in each of the views I have included the foreign key fields: CustomerId and ContactId.
When I do this however the class joins appear to be lost - there's always a null when I drill into either of the objects where it should be pointing to the other one. I have tried to set up what the foreign key field should point to but this doesn't seem to help either.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>().HasRequired(p => p.Customer)
.WithMany()
.HasForeignKey(k => k.CustomerId);
}
Is there a way to establish the connection when overriding the default behaviour?
There is no overriding in this case. If you removed
public DbSet<Customer> Customers { get; set; }
and replaced it with
public IEnumerable<Customer> Customers ()
{
return Database.SqlQuery<Customer>("SELECT * FROM vw_CustomerList");
}
you have completely changed the behavior. The first uses entities and full power of EF. The second is only helper to execute custom SQL. Second without first or without defining entity in OnModelCreating doesn't use Customer as mapped entity at all - it uses it as any normal class (only mapped entities can use features like lazy loading).
Because your Customer is now mapped to view you cannot use your former Customer class used with table. You must define mapping of Customer to a view by cheating EF:
modelBuilder.Entity<Customer>().ToTable("vw_ContactsList"); // EF code fist has no view mapping
Once you have this you can try again using:
public DbSet<Customer> Customers { get; set; }
Unless your view is updatable you will get exception each time you try to add, update or delete any customer in this set. After mapping relation between Customer and Contact mapped to views your navigation properties should hopefully work.
The problem with SqlQuery is the way how it works. It returns detached entities. Detached entities are not connected to the context and they will not lazy load its navigation properties. You must manually attach each Customer instance back to context and to do that you again need DbSet.

Resources