views:

222

answers:

2

I need to generate several reports, and many of them will simply be a table of entities from my database. The entities could be of any type, and I won't always need the entire entity.

My current approach is to create a ViewModel that contains a field of type List<List<string>>, which represents my table, where each row is a List of cells. Then, the view only needs to enumerate through each row and column to create the table.

public class ListReportViewModel
{
    public string Title;
    public List<string> Headings;
    public List<List<string>> Rows;
}

Then I have controller code to populate Headings and Rows:

// Get the entities for the report
var tickets = ( from t in _db.Ticket.Include("Company").Include("Caller")
              select t );    

// Populate the column headings
data.Headings = new List<string>();
data.Headings.Add( "Ticket ID" );
data.Headings.Add( "Company" );
data.Headings.Add( "Caller" );
data.Headings.Add( "Reason for Call" );

// Temporary staging variables
List<List<string>> rows = new List<List<string>>();
List<string> row;

// Populate temporary variables
foreach ( var ticket in tickets )
{
    row = new List<string>();

    row.Add( ticket.TicketID.ToString() );
    row.Add( ticket.Company.Name );
    row.Add( ticket.Caller.FirstName + " " + ticket.Caller.LastName );
    row.Add( ticket.Subject );

    rows.Add( row );
}

// Populate ViewModel field
data.Rows = rows;

Although this works, it seems inefficient. I'm looping through my entire result set just to populate the ViewModel, then the view is going to have to loop through it again to build the report.

My question: Is there a simpler way to do this? If I could get my Linq query to return an IEnumerable<IEnumerable<string>>, then I could just use the line "data.Rows = tickets" and the view would be able to loop through this itself.

I think there must be a better way to do this that I'm not aware of.

+1  A: 

If you are willing to use reflection then try out my project in codeplex mvcrendermodel

It displays a List of objects as a table.

I build the project for easier debugging and it is in general not usefull for production, but if you only have limited traffic it should be ok.

Malcolm Frexner
Your extension method is interesting, and +1 for leading me to use reflection. Thanks! This didn't give me quite what I needed for reports, though.
Stephen Jennings
Nice to hear that it worked for you. If you miss something in it please post a note at codeplex.
Malcolm Frexner
A: 

Here's the solution I came up with after looking through Mathias' suggestion. Not the speediest thing in the world, but quick enough for now.

I changed my ViewModel to use a IEnumerable without a type and a dictionary for headings:

ListReportViewModel.cs

public class ListReportViewModel
{
    public string Title;
    public Dictionary<string,string> Headings;
    public IEnumerable Data;
}

In my controller, I select the entities the normal way, and populate the Headings dictionary to define what I want to report on:

ReportController.cs

var data = new ListReportViewModel();

data.Title = "Closed Calls";

data.Headings = new Dictionary<string, string>();
data.Headings.Add( "TicketID", "ID" );
data.Headings.Add( "Company.Name", "Company" );
data.Headings.Add( "Caller.FirstName", "Caller" );
data.Headings.Add( "Subject", "Reason for Call" );
data.Headings.Add( "ClosedTime", "Closed" );
data.Headings.Add( "ClosedBy.LoginName", "Closed By" );

data.Data = ( from t in _db.Ticket.Include( "Company" ).Include( "Caller" ).Include( "ClosedBy" )
              where !t.Open
              orderby t.ClosedTime ascending
              select t );

return View( "list", data );

Then, the meat of the processing is done by the view to churn through the ViewModel and populate a table:

list.aspx

bool isFirstRow = true;
Type rowType = typeof( System.Data.Objects.DataClasses.EntityObject );
Type propType;
System.Reflection.PropertyInfo propInfo;
object propObject;
string[] propNames;

foreach ( var row in Model.Data )
{
    if ( isFirstRow )
    {
        // Get the type of entity we're enumerating through
        rowType = row.GetType();
        isFirstRow = false;
    }

    // Enumerate through the columns
    foreach ( var kvp in Model.Headings )
    {
        propNames = kvp.Key.Split( '.' );
        propObject = row;
        propType = rowType;

        // Drill down through the entity properties so we can
        // handle properties like "Ticket.Company.Name"
        foreach ( var propName in propNames )
        {
            try
            {
                propInfo = propType.GetProperty( propName );
                propObject = propInfo.GetValue( propObject, null );
                propType = propObject.GetType();
            }
            catch ( NullReferenceException ) { }
        }

        try
        {
            Response.Write( "<td>" + Html.Encode( propObject.ToString() ) + "</td>" );
        }
        catch ( NullReferenceException )
        {
            Response.Write( "<td>--</td>" );
        }

    }
}
Stephen Jennings