Search code examples
c#entity-frameworkasp.net-mvc-5asp.net-identity

MVC5 - ManytoMany Using ApplicationUser Adds new Rows instead of using Existing Ones / Or Throws Error


I building an MVC5 Web App. I have used the Identity Framework. I am trying to make it so the each user has a list of Armies he is associated with. These are to be updated on a page via check boxes.

I have extended The default ApplicationUser class with this additional property.

    public virtual ICollection<Army> Armies { get; set; }

    public ApplicationUser()
    {
        Armies = new HashSet<Army>();
    }

I also have my Army class with these fields. The Army table has been populated with about 10 entries detailing different armies.

public class Army
{
    public int Id { get; set; }
    public String Name { get; set; }
    public String IconLocation { get; set; }
    public virtual ICollection<ApplicationUser> Users { get; set; }

    public Army()
    {
        Users = new HashSet<ApplicationUser>();
    }
}

Then i have set up the relationship in the DbContext

          protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder
            .Entity<ApplicationUser>()
            .HasMany(u => u.Armies)
            .WithMany(t => t.Users)
            .Map(m =>
            {
                m.ToTable("UserArmies");
                m.MapLeftKey("UserId");
                m.MapRightKey("ArmyId");
            });

}

The user can edit these on the Manage User Page, these are displayed as checkbox items.

public class CheckBoxItem
{
    public int Id { get; set; }
    public String Name { get; set; }
    public bool IsChecked { get; set; }
}

The controller calls PopulateArmyCheckBoxList() method to turn the armies into a check box list. (This was originally done using Lists, but is implemented below with dictionary whilst i was trying to fix my problem.) This part of the program works as expected and i display my checkboxes correctly with the relevant boxes ticked.

   private List<CheckBoxItem> PopulateArmyCheckBoxList()
    {
        var userId = User.Identity.GetUserId();
        var userArmies = _context.Users.SingleOrDefault(u => u.Id == userId).Armies.ToDictionary(t=>t.Id, t=>t.Name);

        var allArmies = _context.Armies.ToDictionary(t=>t.Id, t => t.Name);
        var armyCheckBoxList = new List<CheckBoxItem>();


        foreach (var army in allArmies)
        {
            armyCheckBoxList.Add(new CheckBoxItem()
            {
                Id = army.Key,
                Name = army.Value,
                IsChecked = false
            });
        }

        foreach(var army in userArmies)
        {
            var cb = armyCheckBoxList.Find(c => c.Id == army.Key);
            if(cb != null)
                cb.IsChecked = true;
        }

        return armyCheckBoxList;
    }

The viewModel contains a User and a List of CheckBoxItems called Armies. If accessing for the first time, the User == null and the viewModel is populated with data. Otherwise it takes the data and updates the User.

    public async Task<ActionResult> Index(IndexViewModel viewModel)
    {
        var userId = User.Identity.GetUserId();

        if (viewModel.User == null)
        {
            var model = new IndexViewModel
            {
                User = UserManager.FindById(userId),
                Armies = PopulateArmyCheckBoxList()
            };
            return View(model);
        }

        var user = UserManager.FindById(userId);
        user.UserName = viewModel.User.UserName;
        user.Email = viewModel.User.Email;
        user.ProfilePictureLocation = SaveProfileImage(user, viewModel.ProfilePicture);
        UserManager.Update(user);

        ViewBag.StatusMessage = "Your Profile Has Been Updated.";
        viewModel.User = UserManager.FindById(User.Identity.GetUserId());
        return View(viewModel);
    }

The final stage is where it all goes horribly wrong. I need to update the UserArmies table on the database. I have tried to do this by adding the following line of code before calling UserManager.Update(user);

        user.Armies = GetUserArmies(user, viewModel.Armies);

Which calls the following method. First i get a list of Id's and call it selectedArmies. i then fetch a list of armies from the database and put it inside allArmies. userArmies is a blankList of Armies to fill and return.

    private List<Army> GetUserArmies(ApplicationUser user, List<CheckBoxItem> armies)
    {
        var selectedArmies = armies.Where(a => a.IsChecked).Select(a => a.Id).ToList();
        var allArmies = _context.Armies.ToList();

        var userArmies = new List<Army>();

        foreach (var army in selectedArmies)
            userArmies.Add(allArmies.Find(t => t.Id == army));

        return userArmies;
    }

I have had 2 unwanted results. The first is how i am currently. An error occurs saying The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects

When i remove the virtual keyword from the Army class i instead get the following result where the Armies table is first duplicating the Army row under a new Id. And then the new UserArmy is saved to the database under the newly created duplicated of the Exsiting Army. This then causes my checkbox list to double up.

Where am i going wrong? Any help would be appreciated.

ADDITIONAL INFO I also have User Article class the uses the same armies table. This uses very similiar code and works perfectly without creating duplicates. This make me thing it is something to do with sing UserManager and _Context


Solution

  • I think it have found an answer

    I was using UserManager and _context (my own DbContext) to access data, for some reason it wasn't able to conbine the data. By swapping my method around to call the user via my DBContext instead of UserManager i got round the error and managed to fix the problem.