patterncsharpMinor
ListBox for a many-to-many relationship
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
My Database using Entity Framework Code First:
Books, and authors, where one book, can have multiple authors.
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())
{
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
Your view will need to be typed to the view model and you could then write
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.
Then your code in the Post will be:
And I would consider making the Books DbSet lazy loaded.
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.