2

I'm having great difficulties with creating a complex object. I have an EF model with a Consultant table that has one-to-many relationships to a number of other tables. I wanted to use the Consultant object as the model as is because it would be very simple and easy (and as it's done in the NerdDinner tutorial), as I've done with other objects that didn't have one-to-many relationships. The problem is that these relationships cause this error: "EntityCollection already initialized" when I try to post to the Create method.

Several people have advised me to use a ViewModel instead, and I posted a question about this (ViewModels and one-to-many relationships with Entity Framework in MVC?) because I don't really understand it. The problem is the code gets reeeeally ridiculous... It's a far cry from the simplicity I've so far appreciated in MVC.

In that question I forgot to mention that besides the Create method, the Edit method uses the same CreateConsultant method (the name may be misleading, it actually populates the Consultant object). And so in order not to have additional, say "Programs" added when editing, I needed to complicate that method further. So now it looks like this:

private Consultant CreateConsultant(ConsultantViewModel vm, Consultant consultant) //Parameter Consultant needed because an object may already exist from Edit method.
        {
            consultant.Description = vm.Description;
            consultant.FirstName = vm.FirstName;
            consultant.LastName = vm.LastName;
            consultant.UserName = User.Identity.Name;

            if (vm.Programs != null)
                for (int i = 0; i < vm.Programs.Count; i++)
                {
                    if (consultant.Programs.Count == i)
                        consultant.Programs.Add(vm.Programs[i]);
                    else
                        consultant.Programs.ToList()[i] = vm.Programs[i];
                }
            if (vm.Languages != null)
                for (int i = 0; i < vm.Languages.Count; i++)
                {
                    if (consultant.Languages.Count == i)
                        consultant.Languages.Add(vm.Languages[i]);
                    else
                        consultant.Languages.ToList()[i] = vm.Languages[i];
                }
            if (vm.Educations != null)
                for (int i = 0; i < vm.Educations.Count; i++)
                {
                    if (consultant.Educations.Count == i)
                        consultant.Educations.Add(vm.Educations[i]);
                    else
                        consultant.Educations.ToList()[i] = vm.Educations[i];
                }
            if (vm.WorkExperiences != null)
                for (int i = 0; i < vm.WorkExperiences.Count; i++)
                {
                    if (consultant.WorkExperiences.Count == i)
                        consultant.WorkExperiences.Add(vm.WorkExperiences[i]);
                    else
                        consultant.WorkExperiences.ToList()[i] = vm.WorkExperiences[i];
                }

            if (vm.CompetenceAreas != null)
                for (int i = 0; i < vm.CompetenceAreas.Count; i++)
                {
                    if (consultant.CompetenceAreas.Count == i)
                        consultant.CompetenceAreas.Add(vm.CompetenceAreas[i]);
                    else
                        consultant.CompetenceAreas.ToList()[i] = vm.CompetenceAreas[i];
                }

            string uploadDir = Server.MapPath(Request.ApplicationPath) + "FileArea\\ConsultantImages\\";
            foreach (string f in Request.Files.Keys)
            {
                var filePath = Path.Combine(uploadDir, Path.GetFileName(Request.Files[f].FileName));
                if (Request.Files[f].ContentLength > 0)
                {
                    Request.Files[f].SaveAs(filePath);
                    consultant.Image = filePath;
                }

            }
            return consultant;
        }

This is absurd, and it's probably due to my incompetence, but I need to know how to do this properly. Just the answer "use a ViewModel" obviously won't suffice, because that's what got me into this trouble to begin with. I want the simplicity of the simple entity object as model but without the "EntityCollection has already been initialized" error. How do I get around this?

Of course, if I'm just doing the ViewModel strategy the wrong way, suggestions on that are welcome too, but mainly I want to know what is causing this error if I do it the simple "NerdDinner" simple object way. Please keep in mind also that the View in question is restricted to authorized users of the site. I would love to do it the "correct" way, but if using ViewModels implies having code that is this hard to maintain, I'll forgo it...

Please help!

UPDATE:

Turns out this code doesn't even work. I just checked after calling Edit to update values, and it doesn't update them. So only the Create part works.

This is the part that doesn't work:

consultant.Programs.ToList()[i] = vm.Programs[i];

I sort of had a hunch I couldn't use ToList and update an item in the EntityCollection. But this makes it even harder. So now I don't know how to do it with the entity directly, which I would prefer (see above). And I don't know how to get this ViewModel stuff working, let alone get it clean...

Any ideas? There must be something really wrong here, and I'm hoping someone will spot how I've just missed something simple that turns all of this code on its head!

Community
  • 1
  • 1
Anders
  • 12,556
  • 24
  • 104
  • 151
  • Where specifically does the error happen? – Brian Mains Feb 24 '11 at 16:30
  • It happens if I do not have the code above (which is the attempt at using a ViewModel instead), but rather receive a Consultant object back from the view to the Post Create method. It never reaches the method (meaning I can't put a breakpoint to follow it, because it never gets there). Rather the code stops inside the EF model designer code, at either of the collections (the many side of the relationships), e.g. "Programs", and will say that this EntityCollection has already been initialized". – Anders Feb 24 '11 at 16:49

1 Answers1

1

Ok, so in my experience the EF stuff doesn't easily play nicely when used on the wire, full stop (meaning, it's not a good format for passing data in whether you're in MVC or WCF, or whatever). It's an EF issue, to me, not a MVC issue because the problem you're experiencing with the direct use of your EF models has to do w/ how they're tracked in the EF objectcontext. I've been told that there's solutions to that involving re-attaching the passed entity, but I've found it to be more trouble than it's worth; as have others which is why most folks say "use viewmodels" for your input parameters in this situation.

I agree that your code above is kind unpleasant, there's a couple ways to fix it. First, check out AutoMapper, which helps a lot in that you can skip defining the obvious (e.g. when both models have a simple property like "Name" of hte same type & name.

Second, what you're doing w/ the loops is a bit off anyway. You should not assume that your list being posted from the client is the same order, etc as the list that EF is tracking (lists, rather; referring to your Programs, etc). Instead, think of the scenarios that might occur and account for those. I'll use Programs as an example.

1) a consultant has a program, but details about that program have changed. 2) a consultant does not have a program that is in the viewmodel 2a) the program already exists in the database somewhere, just not part of this consultant 2b) the program is new. 3) a consultant has a program that's not in the viewmodel (something you don't currently account for).

I don't know what you want to have happen in each of those cases (i.e. is the viewmodel complete and canonical, or is the entity model, during an update?), but let's say you're looping over your viewmodel's Programs as you do above. For the scenario 1 (which seems to be your main problem), you will want to do something like (using AutoMapper):

var updated = vm.Programs[i];    
var original = consultant.Programs.SingleOrdefault(p=>p.ID == uppdated.ID);
Mapper.Map(updated,original);
yourEfContext.SaveChanges(); // at some point after this, doesn't have to be inside the loop

EDIT: one other thought that might be useful to you is that I when you fetch your consultant out of the database (not sure what mechanism you use for that), make sure you call Include on the collections that you're going to update as well. Otherwise, each of those iterations you do will be another round-trip to the database, which obviously you could avoid if you just used Include to eager-load them all in one shot.

Paul
  • 35,689
  • 11
  • 93
  • 122
  • Ok, well this AutoMapper seems to pop up everywhere, so I assume I have to look into that. But could you just clarify: Do I have to first map the other way around? (Consultant.Programs -> vm.Programs?) And if so, do I do this in the Get method? And secondly, does the AutoMapper transfer the values of all the properties in the vm.Program object to the consultant.Program object in this mapping? I would really appreciate an extended example with how to create the mapper, map initially, and then map back if you wouldn't mind! Thanks! – Anders Feb 25 '11 at 17:08
  • there's a ton of examples that come with automapper when you pull it doen. but basically you want to create (one time, probably in a clas you call from your App_Start) your maps. The most basic form of that is: Mapper.CreateMap(), but there's all kinds of overloads you'll do if you want to customize how things get mapped (or if things do, since the shape of your classes might not always match). – Paul Feb 25 '11 at 18:52
  • Ok, thanks. What I really would like to know though (I've tried reading a bit about AutoMapper, but it's kind of complex): Can I map the entire Consultant object to a ConsultantViewModel object, including the child collections? And if so, how? I've only been able to find simpler examples (simple properties). – Anders Feb 26 '11 at 00:01
  • http://stackoverflow.com/questions/1789925/c-automapper-nested-collections it's pretty much the same as mapping anything else, as shown on that post. – Paul Feb 26 '11 at 02:15
  • Ok, so basically just map the parent object, and then the child collections directly after that, one after another? Thanks! – Anders Feb 26 '11 at 10:45