11
\$\begingroup\$

I've got this all working, but it seems to be quite long-winded, and I thought I'd post here and see if I'm doing it wrong...

I have a M:M relationship between an Installer and a MasterInstance. The classes (code-first) look like:

public class MasterInstance
{
    .. rest of fields here ..
    public virtual ICollection<Installer> PermittedInstallers { get; set; }
}

public class Installer
{
    .. rest of fields here ..
    public virtual ICollection<MasterInstance> PermittedMasterInstances { get; set; }
}

I want to be able to edit these in my Installer view, using a multi-list box. So, I create a ViewModel for what I need:

public class InstallerViewModel
{
    public Installer Installer { get; set; }
    public List<MasterInstance> PermittedMasterInstances { get; set; }
    public int[] SelectedMasterInstances { get; set; }
}

And then I place this in my view like so so that I can select the instances for my installer:

<div class="editor-field">
    @Html.ListBoxFor(model => model.SelectedMasterInstances,(Model.PermittedMasterInstances).Select(option => new SelectListItem {
        Text = option.Name, 
        Value = option.MasterInstanceId.ToString()
}))
</div>

In the controller, for the edit action, I need to set this view model up:

var installer = context.Installers.Include(i => i.PermittedMasterInstances).Single(x => x.InstallerId == installerId);
            InstallerViewModel model = new InstallerViewModel
                                           {
                                               Installer = installer,
                                               PermittedMasterInstances = context.MasterInstances.ToList(),
                                               SelectedMasterInstances = installer.PermittedMasterInstances.Select(i => i.MasterInstanceId).ToArray()
                                           };
return View(model);

Finally, on the post of the edit, I need to delete any relationships that are no longer there and add the new ones:

// Grab the model from the viewmodel and attach to the context
var installer = installerModel.Installer;
context.Installers.Attach(installer);

// Load the related records (dont know why Lazy Loading wouldn't kick in here)
context.Entry(installer).Collection(i => i.PermittedMasterInstances).Load();
// Iterate and delete existing relationships
var instancesToDelete = installer.PermittedMasterInstances.Where(mi => !installerModel.SelectedMasterInstances.Contains(i.MasterInstanceId)).ToList();
instancesToDelete.ForEach(mi => installer.PermittedMasterInstances.Remove(mi));

// Now loop through an int[] and add those new relations, WITHOUT the pain of fetching them from the DB
foreach (var permittedMasterInstanceId in installerModel.SelectedMasterInstances)
{
    if (!installer.PermittedMasterInstances.Any(pmi => pmi.MasterInstanceId == permittedMasterInstanceId))
    {
        var masterInstance = new MasterInstance {MasterInstanceId = permittedMasterInstanceId};
        context.MasterInstances.Attach(masterInstance);
        installer.PermittedMasterInstances.Add(masterInstance);
    }
}

// We're done - save and finish.
context.Entry<Installer>(installer).State = EntityState.Modified;
context.SaveChanges();

So this works... But, it seemed like a lot of effort, is this the right/best way to achieve it?

\$\endgroup\$
  • \$\begingroup\$ This is exactly how ive approached this problem in the past, its a bit ugly but ive never fund a better way \$\endgroup\$ – Not loved Not their people Feb 15 '12 at 21:42
5
\$\begingroup\$

I know this is an old question, but I think the problem is timeless. Many to many associations (i.e. without junction class) in Entity Framework are always independent associations, so you can only establish or remove them by manipulating object collections, not primitive key values. Inefficiency is inherent to the implementation.

But it is not prohibited to have a second context that only contains junction tables.

You could create a context that contains the MasterInstanceInstaller junction table and use this to update the associations in the most efficient way you can get using EF:

var installer = installerModel.Installer;

var junctions = context.MasterInstanceInstallers
                       .Where(x => x.InstallerId == installer.InstallerId)
                       .ToList();

// Delete deselected instances.
foreach(var mi in junctions
                  .Where(x => !installerModel.SelectedMasterInstances
                                             .Contains(x.MasterInstanceId)))
{
    context.MasterInstanceInstallers.Remove(mi);
}

// Add newly selected instances.
foreach(int instanceId in installerModel.SelectedMasterInstances
                                        .Except(junctions.Select(j => j.MasterInstanceId)))
}
    context.MasterInstanceInstallers.Add(new MasterInstanceInstaller
                                             {
                                                InstallerId = installer.InstallerId,
                                                MasterInstanceId = instanceId
                                             }
                                        );
}
context.SaveChanges();

Now, if necessary you can populate the updated many to many association through the main context.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.