views:

28

answers:

2

The standard MVC example to draw an item with the appropriate View Template is:

Html.DisplayFor(m => m.Date)

If the Model object has a property named Date of type DateTime, this returns a string with the HTML from the Display/DateTime.ascx template.

Suppose you wanted to do the same thing, but couldn't use the strongly-typed version - you didn't know the Model's type for this View at compile time. You use the older:

Html.Display("Date");

So here's the hard part.

Suppose the Model is IEnumerable. You don't know what those objects are at compile-time, but at run-time they happen to be objects with a Date of type DateTime again, like:

public class ModelClass
{
    public DateTime Date { get; set; }
}

Now suppose you want your View to iterate over those objects and render each out. If all you cared about was the value you could do this:

<%
StringBuilder sb = new StringBuilder();
foreach(object obj in (IEnumerable<object>)Model)
{
    Type type = obj.GetType();

    foreach(PropertyInfo prop in type.GetProperties())
    {
        // TODO: Draw the appropriate Display PartialView/Template instead
        sb.AppendLine(prop.GetValue(obj, null).ToString());
    }
}
%>
<%= sb.ToString() %>

I'm obviously taking some shortcuts to keep this example focused.

Here's the point - how do I fulfill that TODO I've written for myself? I don't just want to get the value - I want to get it nicely formatted like Html.Display("Date"). But if I just call Html.Display("Date"), it inspects the Model, which is an IEnumerable, for a property named Date, which it of course does not have. Html.Display doesn't take an object as an argument to use as the Model (like Html.Display(obj, "Date"), and all the classes and methods I can find that lie underneath appear to be internal so I can't tweak and call into them directly.

There must be some simple way to accomplish what I'm trying to do, but I can't seem to find it.

Just to make sure I'm being clear - here's an example of the code of DateTime.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<System.DateTime>" %>
<%= Model.ToString("MM/dd/yyyy") %>

And so, ideally, the output from this View that can take any Model, but in this case a list of 3 of these ModelClass objects above, would be:

11/10/2001 11/10/2002 11/10/2003

Because the code would find the Display PartialView for DateTime and render it appropriately for each.

So - how do I fulfill the TODO?

+1  A: 

Have a look at the template code in this excellent post from Phil Haack. It seems to come close to what you are looking for: http://haacked.com/archive/2010/05/05/asp-net-mvc-tabular-display-template.aspx

Clicktricity
Wow, excellent. I think this is a great answer, but there's a major flaw in that code - suppose you want to draw an empty table? In other words, suppose the list of elements is passed in, and it's empty - you still want to draw the header, right?The lambda expression m => m[0] is assuming that what's passed in is a list (which is why he's using IList not IEnumerable), and more importantly it's assuming there's at least one element in that list! That code is going to crash for an empty list, which seems like a case that needs to be handled.
Chris Moschini
I found the fix for the linked code. Instead of fetching the type off the first element, you can use IEnumerable and do the following:Type itemType = list.GetType().GetGenericArguments()[0];ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, itemType);You can then foreach through modelMetadata.Properties.
Chris Moschini
A: 

I've found one potential solution to this but I'm not in love with it; it requires using several file-based templates, meaning you can't abstract this easily into a code library for use in multiple projects.

The View:

<%
StringBuilder sb = new StringBuilder();

Type itemType = Model.GetType().GetGenericArguments()[0];

sb.AppendLine("<table>");

// Pass in the Model (IEnumerable<object>)'s generic item type as
// the Model for a PartialView that draws the header
sb.Append(Html.Partial("DisplayTableHead", itemType));

foreach(object item in (IEnumerable<object>)Model)
{
    sb.Append(Html.Partial("DisplayTableRow", item));
}

sb.AppendLine("</table>");
%>
<%= sb.ToString() %>

Views/Shared/DisplayTableHead.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Type>" %>
<tr>
<%
foreach (PropertyInfo prop in Model.GetProperties())
{
%>
<th><%= prop.Name %></th>
<%
}
%>
</tr>

Views/Shared/DisplayTableRow.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<tr>
<%
Type modelType = Model.GetType();

foreach (PropertyInfo modelField in modelType.GetProperties())
{
%>
<td><%= Html.Display(modelField.Name) %></td>
<%
}
%>
</tr>

But I now see the major flaw in this solution, which is that Clicktricity's posted solution acknowledges details in the ModelMetadata - like whether that particular property is set for display, whether it's complex or not, etc.

Chris Moschini
I wonder a bit if MVC should have 4 template types - edit, display, editRow, displayRow. That way when you ask the framework to draw an object as a table row, it gets the right template for that type. You could presumably hack something like this in by having it check for the existence of a DisplayTableRow<Type>.ascx, and fall back to the generalized DisplayTableRow template.
Chris Moschini