Qudus
Qudus

Reputation: 1520

How to populate dropdown field after multiple dropdown values are saved

I have a profile page that will allow the user to edit, select and save multiple skills they are good at. I am able to load the skills to the dropdown from the database, but it doesn't populate the field after it is saved, instead, the value posted is removed from the list of the database dropdown values, and that value/values is/are not shown in the dropdown list when the profile page is reloaded.

I also noticed when I log in with a different user, the posted dropdown values would have also been removed from the dropdown skills selection of the different user.

This is the ApplicationUser class which extends the IdentityUser class. The skills field allows the user to fill multiple skills. This is the class.

public class ApplicationUser : IdentityUser
{
    public ApplicationUser()
    {
        Skills = new List<Skill>();
    }
    public string Name { get; set; }
    public ICollection<Skill> Skills { get; set; }
}

This is the skill model.

public class Skill
{
    public int SkillId { get; set; }
    public string SkillType { get; set; }
}

This is the view model.

public class ProfileViewModel
{
    [Required]
    [DataType(DataType.Text)]
    public string Name { get; set; }

    public List<int> SelectedSkillIds { get; set; }

    public MultiSelectList Skills { get; set; } 
}

This is the onGet profile controller. I'm not totally sure I have any idea of what I am doing here.

public async Task<IActionResult> Profile()
{
    var user = await _userManager.GetUserAsync(User);
    if (user == null)
    {
        return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
    }

    var profileModel = new ProfileViewModel
    {
        Name = user.Name,
        Skills = new MultiSelectList(_context.Skills.OrderBy(x => x.SkillId), "SkillId", "SkillType", selected),
        SelectedSkillIds = user.Skills.Select(x => x.SkillId).ToList()
    };
    return View(profileModel);
}

This is the onPost profile controller.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Profile(ProfileViewModel profileModel)
{
    var user = await _userManager.GetUserAsync(User);
      
    if (user == null)
    {
        return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
    }
    if (ModelState.IsValid)
    {
        if (user.Name != profileModel.Name)
        {
            user.Name = profileModel.Name;
        }
     
        foreach (var skillID in profileModel.SelectedSkillIds)
        {
            user.Skills.Add(new Skill { SkillId = skillID });
        }
           
        await _userManager.UpdateAsync(user);
        await _signInManager.RefreshSignInAsync(user);
        return RedirectToAction("Profile", "Account");
        }
    }

This is the view.

<select data-placeholder="Select your skils" multiple class="chosen-select" asp-for="SelectedSkillIds" asp-items="Model.Skills"></select>

Upvotes: 0

Views: 142

Answers (2)

Qudus
Qudus

Reputation: 1520

The implementation of Zhi's answer saved the skills for the user successfully. Alternatively, I was also able to post the skills with this in the onPost controller.

if (profileModel.SelectedSkillIds != null && profileModel.Skills != user.Skills)
{ 
    List<Skill> userSkills = new List<Skill> { };
    foreach (var skillID in profileModel.SelectedSkillIds)
    {
        userSkills.Add(_context.Skills.FirstOrDefault(x => x.SkillId == skillID));
    }
    user.Skills = userSkills;
}

However, I realized newly added skills were being added to removed ones when the page is returned. To fix that, I simply remove all the skills that are not selected in the options but previously saved for the user in the database.

if (profileModel.SelectedSkillIds != null && profileModel.Skills != user.Skills)
{
    List<Skill> tempSkills = new List<Skill> { };
    foreach (var skillID in profileModel.SelectedSkillIds)
    {
        var skill = _context.Skills.Find(skillID);
        if (skill != null)
        {
            try
            {
                user.Skills.Add(skill);
                tempSkills.Add(skill);
            }
            catch (Exception ex)
            {
                ModelState.AddModelError("", ex.Message);
                return View();
             }
         }
    }
    var allSkills = _context.Skills.ToList();
    var skills2remove = allSkills.Except(tempSkills);
    foreach (var sk in skills2remove)
    {
        try
        {
            user.Skills.Remove(sk);
        }
        catch(Exception ex)
        {
            ModelState.AddModelError("", ex.Message);
            return View();
        }
    }
}

This article was useful and could help anybody with similar issue.

Upvotes: 0

Zhi Lv
Zhi Lv

Reputation: 21383

if (ModelState.IsValid)
{
    if (user.Name != profileModel.Name)
    {
        user.Name = profileModel.Name;
    }
 
    foreach (var skillID in profileModel.SelectedSkillIds)
    {
        user.Skills.Add(new Skill { SkillId = skillID });
    }
       
    await _userManager.UpdateAsync(user);
    await _signInManager.RefreshSignInAsync(user);
    return RedirectToAction("Profile", "Account");
    }
}

I think the issue is related to the above code. By using it, it will create some new Skills which just contain the SkillId value (without setting the SkillType. The New operator is used to create new items in the Database, but if we want to find the existing Items from the database, we can't use it.). So, when re-rendering, the dropdownlist doesn't contain the values.

To solve this issue, when updated the ApplicationUser, you could query the Skills Table to find the related Skills, and then assign it to the ApplicationUser. Code like this:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Profile(ProfileViewModel profileModel)
{
    var user = await _userManager.GetUserAsync(User);
      
    if (user == null)
    {
        return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
    }
    if (ModelState.IsValid)
    {
        if (user.Name != profileModel.Name)
        {
            user.Name = profileModel.Name;
        }
      
        //query the database to find the related skills

        var selectedSkills = _context.Skills.Where(c => profileModel.SelectedSkillIds.Contains(c.SkillId)).ToList();

        //assign the value to the user
        user.Skills = selectedSkills;

        await _userManager.UpdateAsync(user);
        await _signInManager.RefreshSignInAsync(user);
        return RedirectToAction("Profile", "Account");
        }
    }

After redirecting to the Profile, you could add debugger to check whether the data is correct before return to the view.

Upvotes: 1

Related Questions