Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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 where shorter, smarter, or even made generic somehow, so that it's easy to create MultiSelectLists in future projects..

Here is how i've done it, (Nothing fancy)

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"])

Hope i posted this in the right forum!

share|improve this question
    
This question is on-topic ;-) but note that Auther should be Author. –  codesparkle Nov 11 '12 at 19:22
    
Noted, and corrected :) –  BjarkeCK Nov 11 '12 at 20:08
add comment

1 Answer

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; }
}
share|improve this answer
    
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 :) –  BjarkeCK Nov 11 '12 at 21:41
    
@BjarkeCK Union might be an option you could consider. I've updated my answer for it's usage –  dreza Nov 11 '12 at 22:04
    
Wow there's a lot of unfamiliar code there, lazy load and IEqualityComparer. i willd need to do some research on those, thanks :) –  BjarkeCK Nov 11 '12 at 22:11
    
But i can see that it is not generic, can you make it generic ? –  BjarkeCK Nov 11 '12 at 22:30
    
Instead of IEqualityComparer<Book> just go IEqualityComparer<T> ? I'll research, then ask.. :p –  BjarkeCK Nov 11 '12 at 22:31
show 1 more comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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