Monday, June 14, 2010

ASP MVC 2.0 - Master Detail Views


I recently had a person asking me how to make master-detail views in ASP.NET MVC 2.0.
As I often do when I think this is a common problem I introduce them to Google search.
The person in question came back and said that he could not find any good posts
(I am sure they are out there) but it prompted me to write this post.

What is this post trying to achieve?



This is a walk through how to create a master-detail page using ASP.NET MVC 2.0.
Below we see the rendered output I am aiming for (a CSS or two might make wonders).


"Master" and "Details" in the image above does not require a lot of explaining but the "Details-Details" might need a few words.
Details-Details is basically a third tables in the database holding user names and user information.





The database structure that we have looks like this



Project(s) have project_bids which in turn have a bid creator (user).



The first step is to create LINQ to SQL Classes for the database



Now pull the needed tables onto the LINQ design surface to generate the classes
(this is standard Visual Studio operation so I will not elaborate on this here).





Since our final rendered view consists of data from 3 seperate ViewModels/objects we need to create views for each of those models.
We need one full view, 2 partial views as well as the corresponding 3 ViewModel classes.
You can see the class diagram for the 3 ViewModels and how they interact below.





You can see how the solution looks and where the different files are placed below. (please ignore the HomeController it is not relevant to this context).





The first partial view is for the user name link that is rendered on the project_bid row ("Details-Details").
The user table contains far more columns than we need for this user link,
so we will create a new class/ViewModel (SimpleUserViewModel) which only contains the needed data to create a user link.




public class SimpleUserViewModel
{
    public string Username { get; set; }
    public Guid ID { get; set; }
}



The view that renders this class is located at Views/Shared/UserLink.ascx and looks like this (since the user link will be useful everywhere in the solution,
we will place the view in the Shared folder).


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Appinux.ReverseAuction.Models.SimpleUserViewModel>" %>

<%: Html.ActionLink(Model.Username, "Details","User", new { id = Model.ID }, null) %>

The next higher level in the class hierarchy is ProjectBidsViewModel.

(note the Bidder which type is SimpleUserViewModel)
public class ProjectBidViewModel 
{
    public SimpleUserViewModel Bidder { get; set; }
    public Double BidAmount { get; set; }
    public DateTime BidDate { get; set; }
}

Before we look at the code for the View specific to the ProjectBidViewModel we need to have a look at the ProjectViewModel.
The definition of the ProjectViewModel will affect our implementation of the view specific to the ProjectBidViewModel.




public class ProjectViewModel
{
    public string Name { get; set; }
    public string Description { get; set; }
    public SimpleUserViewModel Owner { get; set; }
    public DateTime BiddingStartDate { get; set; }
    public DateTime BiddingEndDate { get; set; }
    public DateTime ProjectLauchDate { get; set; }
    public DateTime ProjectDeadline { get; set; }
    public float MaxAllowedBid { get; set; }
    public string Status { get; set; }
    public List<projectbidviewmodel> ProjectBids { get; set; }
}

If you closely at the class definition of ProjectViewModel you will note that ProjectBids is a List<ProjectBidViewModel>.
We need to make sure that the strongly typed view of the ProjectBid is of the same type.
(The view is located at Views/Project/ProjectBids.ascx).



<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<List<Appinux.ReverseAuction.Models.ProjectBidViewModel>>" %>
    <table>
        <tr>
            <th>
                BidDate
            </th>
            <th>
                BidAmount
            </th>
        </tr>

    <% foreach (var item in Model) { %>
    
        <tr>
            <td>
                <%: String.Format("{0:g}", item.BidDate) %>
            </td>
            <td>
                <%: item.BidAmount %>
            </td>
            <td>
                <% Html.RenderPartial("UserLink", item.Bidder); %>
            </td>
        </tr>
    
    <% } %>

    </table>

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

Also worth noting is the call to Html.RenderPartial("UserLink",item.Bidder); which calls the first partial view we created - UserLink.ascx.





Finally the full view of Project which is located at Views/Project/Details.aspx.

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<Appinux.ReverseAuction.Models.ProjectViewModel>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
 <head runat="server">
     <title>Details</title>
 </head>
 <body>
  <table border=1>
   <tr>
    <td>Name</td>
    <td><%: Model.Name %></td>
   </tr>
   <tr>
    <td>Description</td>
    <td> <%: Model.Description %></td>
   </tr>
   <tr>
    <td>BiddingStartDate</td>
    <td><%: String.Format("{0:g}", Model.BiddingStartDate) %></td>
   </tr>
   <tr>
    <td>BiddingEndDate</td>
    <td><%: String.Format("{0:g}", Model.BiddingEndDate) %></td>
   </tr>
   <tr>
    <td>ProjectLauchDate</td>
    <td><%: String.Format("{0:g}", Model.ProjectLauchDate) %></td>
   </tr>
   <tr>
    <td>ProjectDeadline</td>
    <td><%: String.Format("{0:g}", Model.ProjectDeadline) %></td>
   </tr>
   <tr>
    <td>MaxAllowedBid</td>
    <td><%: Model.MaxAllowedBid %></td>
   </tr>
   <tr>
    <td>Status</td>
    <td><%: Model.Status %></td>
   </tr>
  </table>  
  <br />
  Project Bids:
  <% Html.RenderPartial("ProjectBids", Model.ProjectBids); %>
  <p>
   <%: Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> |
   <%: Html.ActionLink("Back to List", "Index") %>
  </p>
 </body>
</html>


Also here note the Html.RenderPartial call.



Finally a little bit about the controller implementation.
Since this controller was created to illustrate as simple as possible how to build a master-detail view in ASP.NET MVC,
I have not focused on proper architecture using Depedency Injection, Repository Patterns etc...
(read more here
and here).



Without further ado I present the controller.




public ActionResult Details(Guid id)
{
    Guid project_id = id;   // needed due to conflicting variables 
    ReverseAuctionDBDataContext db = new ReverseAuctionDBDataContext(); 

    var projects = from p in db.projects
                   where p.id == project_id
                   join u in db.users on p.owner_user_id equals u.id
                   select
                   new ProjectViewModel
                   {
                       Name = p.name,
                       Description = p.description,
                       BiddingStartDate = p.bidding_start_date,
                       BiddingEndDate = p.bidding_end_date,
                       ProjectLauchDate = p.project_start_date,
                       ProjectDeadline = p.project_deadline,
                       Owner = new SimpleUserViewModel 
                       { 
                           ID = u.id, 
                           Username = u.user_name 
                       }
                   };

    var bids = from b in db.project_bids
               where b.project_id == project_id
               join u in db.users on b.user_id equals u.id
               select new ProjectBidViewModel 
               { 
                   BidAmount = b.bid_amount, 
                   BidDate = b.bid_date, 
                   Bidder = new SimpleUserViewModel 
                   { 
                       ID = u.id, 
                       Username = u.user_name 
                   } 
               };

    ProjectViewModel project = projects.Single&lt;ProjectViewModel&gt;();
    project.ProjectBids = bids.ToList&lt;ProjectBidViewModel&gt;();
    return View(project);
}

First we get project then the bids for that project and create the full object.
The only other thing that is changed from default code generated controller is the type of the id parameter which here is a Guid rather than an int.

UPDATE

I have added a series of postings that talk about ASP.NET MVC ,Dependency Injection and Inversion of control.

They are available here:

Refactoring ASP MVC 2 to use Dependecy Injection (part1)
Refactoring ASP MVC 2 to use Dependecy Injection (part2)

5 comments:

picapau said...

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,

Kenneth Thorman said...

I have attempted to answer your question in a new blog posting located at http://kenneththorman.blogspot.com/2010/12/aspnet-mvc-master-detail-list-view-with.html

Anonymous said...

"Master" and "Details" in the image above does not require a lot of explaining
==================
I am a beginer so could u plz provide example of master and detail only

Kenneth Thorman said...

You can find a simple master detail at http://kenneththorman.blogspot.com/2010/12/aspnet-mvc-master-detail-list-view-with.html

Regards
Kenneth

Anonymous said...

Thanks, but what I want and hope you can help me
Master-detail CRUD example

for example


1- Order:
with OrderID (int, key), OrderDate,CustomerID,Notes
2-OrderDetail:
with OrderDetail(int, key), OrderID ,ItemID,Quantity,Price