HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

ListBox for a many-to-many relationship

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
relationshipforlistboxmany

Problem

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 Books { get; set; }
    public DbSet 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 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 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();

//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())
{

Solution

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
    {
        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 _books;
    public DbSet Books 
    { 
       get { return _books ?? (_book = new DbSet()); } // TBH not 100% sure you can set DbSet like this but worth a shot 
       set { _books = value; } 
    }

    public DbSet Authors { get; set; }
}

Code Snippets

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);
}
Html.ListBox("SelectedBooks",Model.BookList);
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();
        }
    }
// Take the values common to both lists based on the EqualityComparer
    edit.Books.Union(author.SelectedBooks, new BookEqualityComparer ());
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; }
}

Context

StackExchange Code Review Q#18466, answer score: 3

Revisions (0)

No revisions yet.