6
\$\begingroup\$

I'm looking for a good and easy to use solution for creating MultiSelectLists in MVC for many-to-many relationships.

I have this following example code and it works fine, but it just takes a lot of code. It would be cool if it were shorter, smarter, or even made generic somehow, so that it's easy to create MultiSelectLists in future projects.

My Database using Entity Framework Code First:

Books, and authors, where one book, can have multiple authors.

public class DataContext : DbContext
{
    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
}
public class Book
{
    public int Id { get; set; }
    public string Name { get; set; }

    //Allows multiple authors for one book.
    public virtual ICollection<Author> Authors { get; set; }
}
public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }

    [NotMapped]
    public int[] SelectedBooks { get; set; }
    public virtual ICollection<Book> Books { get; set; }
}

Controller

    public ActionResult Edit(int id = 0)
    {
        Author author = db.Authors.Find(id);
        if (author == null)
        {
            return HttpNotFound();
        }
        ViewData["BooksList"] = new MultiSelectList(db.Books, "Id", "Name", author.Books.Select(x => x.Id).ToArray());

        return View(author);
    }

    [HttpPost]
    public ActionResult Edit(Author author)
    {
        ViewData["BooksList"] = new MultiSelectList(db.Books, "Id", "Name", author.SelectedBooks);

        if (ModelState.IsValid)
        {
            //Update all the other values.
            Author edit = db.Authors.Find(author.Id);
            edit.Name = auther.Name;

        //------------    
            //Make adding items possible
            if (edit.Books == null) edit.Books = new List<Book>();

            //Remove the old, add the new, instead of finding out what to remove, and what to add, and what to leave be.
            foreach (var item in edit.Books.ToList())
            {
                edit.Books.Remove(item);
            }
            foreach (var item in author.SelectedBooks)
            {
                edit.Books.Add(db.Books.Find(item));
            }
        //------------- 
        // This is the code i want to simplify
        // Would be cool with a generic extention method like this:
        // db.ParseNewEntities(edit.Books, author.SelectedBooks);
        // I just don't know how to code it.  



            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(author);
    }

View

    @Html.ListBox("SelectedBooks",(MultiSelectList)ViewData["BooksList"])
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Rather than using ViewData I might consider using ViewBag (if on MVC 3 and above) or even wrapping Author within a ViewModel. That what you won't need the cast on the view (if using a viewModel).

Otherwise your MultiSelectList code is only one line. Don't see you getting much better than that :)

Your viewmodel might look like

public class AuthorViewModel 
{
   public Author BookAuthor { get; set; }
   public MultiSelectList BookList { get; set; }
}

[HttpGet]
public ActionResult Edit(int id = 0)
{
    Author author = db.Authors.Find(id);
    if (author == null)
    {
        return HttpNotFound();
    }

    var viewModel = new AuthorViewModel 
    {
       BookAuthor = author;
       BookList = new MultiSelectList(db.Books, "Id", "Name", author.Books.Select(x => x.Id).ToArray());
    }

    return View(viewModel);
}

Your view will need to be typed to the view model and you could then write

Html.ListBox("SelectedBooks",Model.BookList);

I'm not 100% sure then if you need to re-bind it back on the post if there is a problem. If you do, I would consider doing that after the Model.IsValid unless of course your "Index" action usings the ViewData["BookList"] value?

EDIT: Ok, what about this then.

    internal class BookEqualityComparer : IEqualityComparer<Book>
    {
        public bool Equals(Book x, Book y)
        {
            return x.Id == y.Id;
        }

        public int GetHashCode(Book obj)
        {
            return obj.GetHashCode();
        }
    }

Then your code in the Post will be:

    // Take the values common to both lists based on the EqualityComparer
    edit.Books.Union(author.SelectedBooks, new BookEqualityComparer ());

And I would consider making the Books DbSet lazy loaded.

public class DataContext : DbContext
{
    private DbSet<Book> _books;
    public DbSet<Book> Books 
    { 
       get { return _books ?? (_book = new DbSet<Book>()); } // TBH not 100% sure you can set DbSet like this but worth a shot 
       set { _books = value; } 
    }

    public DbSet<Author> Authors { get; set; }
}
\$\endgroup\$
6
  • \$\begingroup\$ True, but i actually don't mind casting the values, the real is in the [HttpPost] method.. I've updated the question with some inline comments to show you what part i'm interested in shortening :) \$\endgroup\$
    – BjarkeCK
    Nov 11, 2012 at 21:41
  • \$\begingroup\$ @BjarkeCK Union might be an option you could consider. I've updated my answer for it's usage \$\endgroup\$
    – dreza
    Nov 11, 2012 at 22:04
  • \$\begingroup\$ Wow there's a lot of unfamiliar code there, lazy load and IEqualityComparer. i willd need to do some research on those, thanks :) \$\endgroup\$
    – BjarkeCK
    Nov 11, 2012 at 22:11
  • \$\begingroup\$ But i can see that it is not generic, can you make it generic ? \$\endgroup\$
    – BjarkeCK
    Nov 11, 2012 at 22:30
  • \$\begingroup\$ Instead of IEqualityComparer<Book> just go IEqualityComparer<T> ? I'll research, then ask.. :p \$\endgroup\$
    – BjarkeCK
    Nov 11, 2012 at 22:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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