Wednesday, December 08, 2010

ASP.NET MVC Master-Detail List view with a LINQ twist


The source code for this blog posting can be downloaded from this link. MasterDetailsWithALinqTwist.zip

I was asked the following question:


"I agree with that person that told you that is difficult to find a good sample for Master Detail views, in particular for people who are begining with ASP.NET MVC 2.
Your post is a quit good intent to help people like me, but I think it's a little complicated for beginners.

I try to find in Google and in specialized books an answer to a simple but, I think, a very usual problem. I'll explain it with an example:

3 files:

1- Books:
with BookID (int, key), BookTitle (string)

2- Authors:
with AuthorId (int, key), AuthorName (string)

3- AuthorBook
with AuthorId, BookId

The last file is a "junction file". We need it because a book has one or more auhtors.

The problem, which solution I can't find it anywhere, is how can I make a list with the book title with its authors, but not repeating the book title.

Thank you for your time,"

This question was posted as a comment on my blog posting ASP MVC 2.0 - Master Detail Views

First I would like to point out that normally in Object Relational Mapping on the OO side you normally do not create objects for the one-many or many-many relations present in the database - unless the relations themselves carry special data or they have special properties and thus can be promoted to "real" objects/classes.

This means that I will ignore the third file mentioned above in the question (3- AuthorBook with AuthorId, BookId).

So I tend to see the above as a Book class with a property IList Authors, like below

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication2.Models
{
 public class Book
 {
  public string Id { get; set; }
  public string Name { get; set; }
  public IList<Author> Authors { get; set; }
 }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MvcApplication2.Models
{
 public class Author
 {
  public string Id { get; set; }
  public string Name { get; set; }
 }
}

For this small sample code I did not bother to wire the code to a database but just created a LibraryRepository class that can be seen below, this class acts as a placeholder for a database.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication2.Models
{
 public class LibraryRepository
 {
  public IList<Book> GetBooks()
  {
   List<Book> books = new List<Book>() 
   {
    new Book() 
    {
     Id = "1",
     Name = "My First Book",
     Authors = new List<Author>() 
     {
      new Author() { Id = "A1", Name = "Author1" },
      new Author() { Id = "A2", Name = "Author2" }
     }
    },
    new Book() 
    {
     Id = "2",
     Name = "My Second Book",
     Authors = new List<Author> () 
     {
      new Author() { Id = "A3", Name = "Author3" },
      new Author() { Id = "A4", Name = "Author4" }
     }
    }
   };
   return books;
  }
 }
}

Now that we have the "database" and the models in place then we need the controller and the view. Ideally the LibraryRepository should be injected in to the controllers constructor using Dependency Injection (which I am not doing here).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcApplication2.Models;

namespace MvcApplication2.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
  public ActionResult Index()
  {
   // Should be passed in via IOC and contructor injection
   LibraryRepository libraryRepository = new LibraryRepository();
   ViewData.Model = libraryRepository.GetBooks();
   return View();
  }
 }
}

And finally the strongly typed view that uses a nifty LINQ expression to aggregate the Authors into a single string for display purposes.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<MvcApplication2.Models.Book>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
 Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Index</h2>

    <table>
        <tr>
            <th></th>
            <th>
                Id
            </th>
            <th>
                Name
            </th>
            <th>
                Authors
            </th>
        </tr>

    <% foreach (var item in Model) { %>
    
        <tr>
            <td>
                <%: Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) %> |
                <%: Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ })%> |
                <%: Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })%>
            </td>
            <td>
                <%: item.Id %>
            </td>
            <td>
                <%: item.Name %>
            </td>
            <td>
                <%: item.Authors.Select(author => author.Name).Aggregate((currentAuthorName, nextAuthorName) => currentAuthorName + ", " + nextAuthorName) %>
            </td>
        </tr>
    
    <% } %>

    </table>

    <p>
        <%: Html.ActionLink("Create New", "Create") %>
    </p>

</asp:Content>

1. Select the names from the Author object into an anonymous object of type
System.Linq.Enumerable.WhereSelectListIterator<MvcApplication2.Models.Author,string>

2. Aggregate the names in the anonymous object (System.Linq.Enumerable.WhereSelectListIterator<MvcApplication2.Models.Author,string>) adding a comma between.

The output ends up like the following.



The MVC design pattern argues that the views should be pretty lightweight and should only deal with presenting data, logic related to readying data for display / and actual formatting of data should ideally be located in the controller ( I am not doing that here ).

Post a Comment