Some Nerdy Stuff

October 28, 2011

Dynamic MVC Indexes and Handling Lists: Plus Displaying Navigation Properties as Links

Filed under: Uncategorized — aaronls @ 6:05 pm

I wanted to make my Index pages more dynamic and not require me to touch/rescaffold them every time my model changed.

To accomplish this I first Edit the Index page to display a template specialized for handling a list of items.  In this case, I am saying “Take the current model(which happens to be a list of Employees) and display it, oh by the way, use the Summary.cshtml template to do so”

@model IEnumerable<MyApp.Models.Employees>
@{
ViewBag.Title = "Employees";
}
<h2>Employees</h2>

@Html.DisplayForModel("Summary")

The Summary template is similar in concept to what was fleshed out here:

http://haacked.com/archive/2010/05/05/asp-net-mvc-tabular-display-template.aspx

However, I took a couple steps to simplify the code.  One issue others had was the m[0] code breaks if presented with an empty list.  Others addressed this differently, but trying their approaches I found that the metadata they retrieved was the DynamicProxy of the model in one case versus the model class itself.  For me, maybe because of my usage of Data annotations, I found that the header fields of the table were in a different order than the data rows, and thus mislabled the data.  So instead, I simply check for empty (Model.Count == 0) and don’t display anything.  I would recommend instead displaying a useful message for this case, such as “No Employees have been entered into the system.”

Rather than trying to pull the meta data out of the generic IList, I just created two more simple templates that handle the header and data rows.  This allows the engine to do its magic in creating the Metadata.

Please forgive the lack of formatting, as the leading spaces are being stripped by the code tags for some reason.  Just copy and paste then Ctrl+K Ctrl+F to format it in Visual studio.  All of these files go in Shared DisplayTemplates as mentioned in the linked article above.

Summary.cshtml:

@functions{
//this was copied from MVC framework source code
bool ShouldShow(ModelMetadata metadata)
{
return metadata.ShowForDisplay
&& metadata.ModelType != typeof(System.Data.EntityState)
//  && !metadata.IsComplexType
&& !ViewData.TemplateInfo.Visited(metadata);
}
}

@model System.Collections.IList
@if (Model == null || Model.Count == 0)
{
<text>@ViewData.ModelMetadata.NullDisplayText</text>
} else {
<table cellpadding="0" cellspacing="0" border="0">
@Html.DisplayFor(m => m[0], "SummaryHeader")

@foreach (var item in Model)
{
@Html.DisplayFor(m => item, "SummaryItem")
}
</table>
}

SummaryHeader.cshtml:

@functions{
//this was copied from MVC framework source code
bool ShouldShow(ModelMetadata metadata)
{
return metadata.ShowForDisplay
&& metadata.ModelType != typeof(System.Data.EntityState)
//  && !metadata.IsComplexType
&& !ViewData.TemplateInfo.Visited(metadata);
}
}

@if (Model == null) {
<text>@ViewData.ModelMetadata.NullDisplayText</text>
}
else {
<tr>
@foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => ShouldShow(pm)))
{
<th>
<span>@prop.GetDisplayName()</span>
</th>
}
</tr>

SummaryItem:

@functions{
//this was copied from MVC framework source code
bool ShouldShow(ModelMetadata metadata)
{
return metadata.ShowForDisplay
&& metadata.ModelType != typeof(System.Data.EntityState)
//  && !metadata.IsComplexType
&& !ViewData.TemplateInfo.Visited(metadata);
}
}

@if (Model == null) {
<text>@ViewData.ModelMetadata.NullDisplayText</text>
}
else {
<tr>
@foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => ShouldShow(pm)))
{
<td>
<span>
@Html.Display(prop.PropertyName)
</span>
</td>
}
</tr>
}

That function at the top could be factored out, but I’ve had limited success with the techniques I’ve seen suggested so far.

Notice that I do not exclude ComplexTypes(such as navigation properties) because the Object template will simply display the SimpleDisplayText for these, which is exactly what I want.  So if I had a Supervisor navigation property on Employee, then it would get displayed, and the value displayed would be the SimpleDisplayText value.  Usually this is a realy long string that is the name of the generated DynmicProxy class, but if you override ToString() in the referenced class, in this case whatever Supervisor is, such that it returns something meaningful, then you get a meaningful display of your navigation properties.

public override string ToString()
 {
   return this.FirstName + " " + this.LastName;
 }

If you want them displayed differently, you can always apply a UIHint data annotation attribute to the navigation property.  I like them to display them as links to the details page of the related entity.

FKLink.cshtml:

@if (Model == null)
{
<text>@ViewData.ModelMetadata.NullDisplayText</text>
}
else
{
@Html.ActionLink(
@ViewData.ModelMetadata.SimpleDisplayText,  //display text for link
"Details",
@ViewData.ModelMetadata.ModelType.Name, //controller
new
{
id = Model.Key
},
null)
}

This requires every model have property called Key which returns the value of its primary key such as in these two examples, depending on your preferred naming convention:

    [ScaffoldColumn(false)]
public int Key { get { return EmployeeKey; } }
[Key]
public int EmployeeKey { get; set; }

 

    [ScaffoldColumn(false)]
public int Key { get { return EmployeeId; } }
     public int EmployeeId { get; set; }

To take advantage of the FKLink template, you could use some of the template helpers like DisplayForModel, but I just add a UIHint to the navigation property which basically says “Anytime you encounter this property, use the FKLink template to display it”:


class Employee{

.......

public int SupervisorKey { get; set; }
[ForeignKey("SupervisorKey ")]
[UIHint("FKLink")]
public virtual Supervisor Supervisor { get; set; }</pre>
}
Advertisements

Blog at WordPress.com.